@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,42 @@
1
+ import type { NetworkCondition, NetworkPreset } from "./conditions.types";
2
+ /**
3
+ * One table for every layer that applies a network condition: the Android emulator console (`emu network
4
+ * speed|delay`, COND-3), the per-lease shaping proxy (COND-4) and the iOS per-app routing (COND-5).
5
+ *
6
+ * Shaping units: `latencyMs` is the added round-trip time, `jitterMs` the +/- spread on it, kbps are
7
+ * kilobits per second (0 = unlimited), `lossPct` is 0-100. The emulator console's `delay` is per packet, so
8
+ * its `min:max` is half the round trip, widened by the jitter.
9
+ *
10
+ * Sources: Apple Network Link Conditioner profiles (edge, 3g, lte, wifi, lossy = "Very Bad Network"),
11
+ * Chrome DevTools "Slow 3G" (400 kbps both ways, 2000 ms), WebPageTest "4G" (9 Mbps, 170 ms RTT).
12
+ */
13
+ export type NetworkShaping = {
14
+ latencyMs: number;
15
+ jitterMs: number;
16
+ downKbps: number;
17
+ upKbps: number;
18
+ lossPct: number;
19
+ };
20
+ /** Arguments for `adb emu network speed <speed>` and `... delay <delay>`. */
21
+ export type EmuNetwork = {
22
+ speed: string;
23
+ delay: string;
24
+ };
25
+ export type NetworkPresetSpec = {
26
+ emu: EmuNetwork;
27
+ shaping: NetworkShaping;
28
+ };
29
+ export declare const NETWORK_PRESET_NAMES: readonly NetworkPreset[];
30
+ export declare const NETWORK_PRESETS: Readonly<Record<NetworkPreset, NetworkPresetSpec>>;
31
+ /** What a condition means for a device: no network at all, or shaped (all zeros = unshaped) with the emulator args. */
32
+ export type NetworkPlan = {
33
+ online: false;
34
+ } | ({
35
+ online: true;
36
+ } & NetworkPresetSpec);
37
+ /**
38
+ * The effective parameters of a condition. `custom` names only what it changes (the rest is unshaped, a
39
+ * direction left out takes the other's rate for the emulator); `flap` resolves to its up phase, because the
40
+ * scheduler (COND-6) drives the transitions as plain online/offline conditions.
41
+ */
42
+ export declare function resolveNetwork(condition: NetworkCondition): NetworkPlan;
@@ -0,0 +1,19 @@
1
+ import { type Result } from "../../../types/src/result";
2
+ import { TEXT_SIZE_NAMES, type TextSize } from "./conditions.types";
3
+ /** Approximate font scale of each iOS content size category (UIFontMetrics body text; 1 = `large`, the default). */
4
+ export declare const TEXT_SIZE_SCALE: Record<(typeof TEXT_SIZE_NAMES)[number], number>;
5
+ /** The largest `font_scale` Android's own settings offer; asking for more is clamped to it. */
6
+ export declare const ANDROID_MAX_FONT_SCALE = 2;
7
+ export declare const ANDROID_MIN_FONT_SCALE = 0.5;
8
+ export type ResolvedTextSize = {
9
+ /** iOS content size category (the nearest one for a number) */
10
+ category: (typeof TEXT_SIZE_NAMES)[number];
11
+ /** font scale (the category's for a name) */
12
+ scale: number;
13
+ };
14
+ /** A name or a scale as both: the category iOS needs and the scale Android needs. */
15
+ export declare function resolveTextSize(size: TextSize): Result<ResolvedTextSize>;
16
+ /** Android's `font_scale` for `scale`, clamped to what the platform takes. */
17
+ export declare const androidFontScale: (scale: number) => number;
18
+ /** The part of an a11y request the platform applied (a skipped setting is not on the device, so not recorded). */
19
+ export declare function appliedA11y<T extends object>(op: T, applied: readonly string[]): Partial<T>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Android fingerprint enrollment for the golden build emulator: set a screen-lock PIN, open the system
3
+ * enrollment wizard, answer its screens (PIN entry, Next/Agree/Done) and present the virtual finger until
4
+ * `dumpsys fingerprint` reports one enrolled. Slow (tens of seconds), so it runs once in the snapshot build
5
+ * and claims start enrolled. Idempotent: an already enrolled device is left alone. Emulator serials only.
6
+ */
7
+ import { type AsyncResult } from "../../../types/src/result";
8
+ import type { Exec } from "../exec";
9
+ /**
10
+ * Whether `dumpsys fingerprint` lists an enrolled fingerprint: the JSON `"prints":[{"id":0,"count":N,…}]` of Android 10+
11
+ * (`count` = fingerprints enrolled for that user) or the older `Enrolled fingerprints: N` / `prints: N` lines.
12
+ */
13
+ export declare function parseFingerprintEnrolled(dump: string): boolean;
14
+ export type TapTarget = {
15
+ label: string;
16
+ x: number;
17
+ y: number;
18
+ };
19
+ /** Centre of the first wizard button in a `uiautomator dump`. */
20
+ export declare function findTapTarget(xml: string): TapTarget | undefined;
21
+ /** The lock-screen credential entry the wizard asks before it starts. */
22
+ export declare function isPinEntry(xml: string): boolean;
23
+ export type EnrollDeps = {
24
+ exec: Exec;
25
+ sleep: (ms: number) => Promise<void>;
26
+ /** the adb binary (`androidTools(env).adb`) */
27
+ adb: string;
28
+ /** wizard rounds before giving up (default 60) */
29
+ maxSteps?: number;
30
+ /** pause after each round (default 700 ms) */
31
+ stepMs?: number;
32
+ };
33
+ export declare function enrollFingerprint(deps: EnrollDeps, serial: string): AsyncResult<{
34
+ alreadyEnrolled: boolean;
35
+ }>;
@@ -0,0 +1,28 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import type { EnsureRunnerBuildInput, RunnerBuild, RunnerBuildError } from "./runner-build.types";
3
+ export declare const RUNNER_SCHEME = "WardenRunner";
4
+ /**
5
+ * The `xcodebuild` argv that builds the runner for one phone. `-destination id=` is what registers the UDID in the
6
+ * provisioning profile; the bundle id goes through `WARDEN_RUNNER_BUNDLE_ID` (the project derives the host app's,
7
+ * the UI-test bundle's and the runner app's ids from it, which `PRODUCT_BUNDLE_IDENTIFIER` would collapse into one).
8
+ */
9
+ export declare function buildRunnerArgv(o: {
10
+ project: string;
11
+ derivedData: string;
12
+ udid: string;
13
+ team: string;
14
+ bundleId: string;
15
+ }): string[];
16
+ /**
17
+ * The derived-data directory `ensureRunnerBuild` would use for the current team, Xcode build and runner sources,
18
+ * without staging, building or taking the build lease: one Xcode version probe, the team lookup and a hash of the
19
+ * sources. It is the identity of a runner build; a resident host that serves another directory serves an old runner.
20
+ */
21
+ export declare function expectedRunnerDerivedData(input: Pick<EnsureRunnerBuildInput, "team" | "configTeamId" | "env" | "home" | "exec" | "loadSources">): AsyncResult<string, RunnerBuildError>;
22
+ /**
23
+ * A runner build signed for `udid`, ready for `xcodebuild test-without-building -xctestrun`. Cached per
24
+ * (team, Xcode build, runner sources) and rebuilt only for a phone the profile does not list yet, or when the
25
+ * profile ends within 24 h. Builds are serialised by a build lease; a waiting warden re-reads the cache and
26
+ * reuses what the holder built. Never installs or launches anything itself.
27
+ */
28
+ export declare function ensureRunnerBuild(input: EnsureRunnerBuildInput): AsyncResult<RunnerBuild, RunnerBuildError>;
@@ -0,0 +1,44 @@
1
+ import type { Exec } from "../exec";
2
+ /** Rebuild when the provisioning profile ends within this long (free teams get 7-day profiles). */
3
+ export declare const PROFILE_MARGIN_MS: number;
4
+ /** When the profile's expiry cannot be read, assume a free team's 7 days minus a day of margin from the build. */
5
+ export declare const UNKNOWN_PROFILE_LIFETIME_MS: number;
6
+ /** `meta.json` next to `dd/` in a cache directory. */
7
+ export type RunnerMeta = {
8
+ /** every phone the profile was built for (a new UDID needs a build: `-destination id=` registers it) */
9
+ udids: string[];
10
+ /** earliest expiry (ms since epoch) of the profiles embedded in the products; null = unreadable */
11
+ profileExpires: number | null;
12
+ builtAt: number;
13
+ bundleId: string;
14
+ };
15
+ export type RebuildReason = "none" | "new_udid" | "profile_expiring" | "bundle_id" | "missing";
16
+ /** `<home>/ios-runner/<team>/<xcodeBuild>-<srcHash>`: holds `dd/` (derived data) and `meta.json`. */
17
+ export declare function runnerCacheDir(home: string, team: string, xcodeBuild: string, srcHash: string): string;
18
+ export declare function derivedDataDir(cacheDir: string): string;
19
+ /** The cache's meta, or undefined when absent or not ours (a half-written file never reads as a hit). */
20
+ export declare function readRunnerMeta(cacheDir: string): RunnerMeta | undefined;
21
+ /** Written last, after the build, through a rename: the meta is what makes a build count. */
22
+ export declare function writeRunnerMeta(cacheDir: string, meta: RunnerMeta): void;
23
+ /** The `.xctestrun` `build-for-testing` wrote under `Build/Products`. */
24
+ export declare function findXctestrun(derivedData: string): string | undefined;
25
+ export type ReuseDecision = {
26
+ reuse: true;
27
+ } | {
28
+ reuse: false;
29
+ because: RebuildReason;
30
+ };
31
+ /** Can the cached build serve `udid` now? */
32
+ export declare function decideReuse(o: {
33
+ meta: RunnerMeta | undefined;
34
+ xctestrun: string | undefined;
35
+ udid: string;
36
+ bundleId: string;
37
+ now: number;
38
+ }): ReuseDecision;
39
+ /** `<key>ExpirationDate</key><date>2026-11-05T01:02:03Z</date>` of a decoded provisioning profile. */
40
+ export declare function parseProfileExpiry(plist: string): number | undefined;
41
+ /** Paths of the `embedded.mobileprovision` of every app `build-for-testing` produced (host app and test runner app). */
42
+ export declare function embeddedProfiles(derivedData: string): string[];
43
+ /** The earliest expiry among the built apps' profiles (`security cms -D`), null when none can be read. */
44
+ export declare function readProfileExpiry(exec: Exec, derivedData: string): Promise<number | null>;
@@ -0,0 +1,7 @@
1
+ import type { RunnerBuildError } from "./runner-build.types";
2
+ /** `Unlock Chris's iPhone to Continue`: xcodebuild waits for the passcode and never says it is locked any other way. */
3
+ export declare const UNLOCK_LINE: RegExp;
4
+ /** The known failure in xcodebuild's / devicectl's output, or undefined for one warden has no fix for. */
5
+ export declare function classifyRunnerOutput(text: string): RunnerBuildError | undefined;
6
+ /** The last `lines` lines of the output: what `build_failed` shows when nothing classifies. */
7
+ export declare function outputTail(text: string, lines?: number): string;
@@ -0,0 +1,17 @@
1
+ import { type AsyncResult, type Result } from "../../../types/src/result";
2
+ import type { RunnerSourceFiles } from "./runner-build.types";
3
+ /**
4
+ * The iPhone runner's Xcode project, as the text files the binary embeds (`ios-runner.bin`: a JSON map of
5
+ * path -> text), the way the `@delacour/warden-e2e` runtime is. Paths are relative to `packages/e2e/native`
6
+ * and keep that layout, because the project reaches the shared `warden-simd` files through `../ios/…`.
7
+ */
8
+ /** Name of the embedded blob (`Bun.embeddedFiles` reports it with a content hash: `ios-runner-<hash>.bin`). */
9
+ export declare const RUNNER_BLOB = "ios-runner.bin";
10
+ /** Read the runner sources from a checkout's `packages/e2e/native` directory. */
11
+ export declare function collectRunnerSources(nativeDir: string): Result<RunnerSourceFiles>;
12
+ export declare function writeRunnerBlob(nativeDir: string, outFile: string): Result<void>;
13
+ /** A path inside the staged root: relative, `/`-separated, no `..`. */
14
+ export declare function isSafeSourcePath(path: string): boolean;
15
+ export declare function parseRunnerBlob(text: string): Result<RunnerSourceFiles>;
16
+ /** The blob embedded in the compiled binary (`Bun.embeddedFiles`, name `ios-runner-<hash>.bin`). */
17
+ export declare function loadEmbeddedRunnerSources(embedded?: readonly Blob[]): AsyncResult<RunnerSourceFiles>;
@@ -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,40 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import type { Exec } from "../exec";
3
+ import type { RunnerBuildError, SigningTeam, TeamSource } from "./runner-build.types";
4
+ /** Env var that names the signing team; wins over `ios.teamId`. */
5
+ export declare const IOS_TEAM_ENV = "WARDEN_IOS_TEAM_ID";
6
+ /** `nz.co.delacour.warden.runner.<team>` (what the Xcode project defaults to too). */
7
+ export declare function defaultRunnerBundleId(team: string): string;
8
+ export type SigningIdentity = {
9
+ sha1: string;
10
+ name: string;
11
+ };
12
+ /** ` 3) 1111…11 "Apple Development: Test User (AB12CD34EF)"` lines of `security find-identity -v -p codesigning`. */
13
+ export declare function parseIdentities(stdout: string): SigningIdentity[];
14
+ /** The DER of every `-----BEGIN CERTIFICATE-----` block in `text`. */
15
+ export declare function certificatesFromPem(text: string): Uint8Array[];
16
+ export declare function sha1Hex(der: Uint8Array): string;
17
+ /**
18
+ * The `OU` of the certificate's subject: for Apple Development certificates the team id. Walks
19
+ * Certificate > TBSCertificate > subject (the issuer's own `OU` is a different element and never read).
20
+ */
21
+ export declare function teamOfCertificate(der: Uint8Array): string | undefined;
22
+ /** The teams the keychain holds a valid `Apple Development` identity for. Read-only: two `security` calls. */
23
+ export declare function listSigningTeams(exec: Exec): AsyncResult<SigningTeam[]>;
24
+ export type ResolvedTeam = {
25
+ team: string;
26
+ source: TeamSource;
27
+ };
28
+ /**
29
+ * The signing team, first hit wins: the explicit one, `WARDEN_IOS_TEAM_ID`, `ios.teamId`, then the keychain when
30
+ * it holds `Apple Development` identities of exactly one team. Several teams is `ios_team_ambiguous` (never a
31
+ * guess: a wrong team signs with the wrong account); none is `no_team`.
32
+ */
33
+ export declare function resolveTeam(input: {
34
+ explicit?: string | undefined;
35
+ configTeamId?: string | undefined;
36
+ env: Record<string, string | undefined>;
37
+ exec: Exec;
38
+ }): AsyncResult<ResolvedTeam, RunnerBuildError>;
39
+ /** The runner's bundle id: the override (checked), else `nz.co.delacour.warden.runner.<team>`. */
40
+ export declare function resolveRunnerBundleId(team: string, configured: string | undefined): string | RunnerBuildError;
@@ -0,0 +1,36 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import { type BuildLockDeps } from "../builds/lock";
3
+ import { type Exec } from "../exec";
4
+ import type { RunnerSourceFiles } from "./runner-build.types";
5
+ /**
6
+ * The runner (XCUITest server) built for the iOS Simulator: no signing, no team, no device registration, so it is one
7
+ * `xcodebuild build-for-testing -destination 'generic/platform=iOS Simulator'` (about 4-10 s) per Xcode build and runner
8
+ * source hash, cached under `<home>/ios-runner/simulator/<xcodeBuild>-<hash>`. Any simulator can then run it
9
+ * (`test-without-building -destination id=<udid>`). Used by the XCUITest accessibility audit (A11Y-4).
10
+ */
11
+ export type SimRunnerBuild = {
12
+ /** the `.xctestrun` inside `derivedData` */
13
+ xctestrun: string;
14
+ derivedData: string;
15
+ /** false: built just now */
16
+ cached: boolean;
17
+ };
18
+ export type EnsureSimRunnerInput = {
19
+ env: Record<string, string | undefined>;
20
+ exec: Exec;
21
+ /** warden's own version (part of the staged-source directory name) */
22
+ version: string;
23
+ loadSources: () => AsyncResult<RunnerSourceFiles>;
24
+ /** serialises builds of one (Xcode, sources); without it two concurrent callers may both build */
25
+ lock?: BuildLockDeps;
26
+ home?: string;
27
+ log?: (line: string) => void;
28
+ timeoutMs?: number;
29
+ };
30
+ export declare function simRunnerCacheDir(home: string, xcodeBuild: string, srcHash: string): string;
31
+ export declare function buildSimRunnerArgv(o: {
32
+ project: string;
33
+ derivedData: string;
34
+ }): string[];
35
+ /** A simulator runner build for the current Xcode and runner sources; built now when the cache has none. */
36
+ export declare function ensureSimRunnerBuild(input: EnsureSimRunnerInput): AsyncResult<SimRunnerBuild>;
@@ -0,0 +1,26 @@
1
+ import { type AsyncResult } from "../../../types/src/result";
2
+ import type { RunnerSourceFiles } from "./runner-build.types";
3
+ export type StagedRunner = {
4
+ /** `<home>/ios-runner/src/<version>-<hash>` */
5
+ dir: string;
6
+ /** the Xcode project inside it */
7
+ project: string;
8
+ /** `<version>-<hash>`: part of the build cache key */
9
+ key: string;
10
+ /** first 10 hex digits of the sources' hash */
11
+ hash: string;
12
+ };
13
+ /** Where the runner builds live: `<home>/ios-runner`. */
14
+ export declare function runnerRoot(home: string): string;
15
+ /** Content hash of the sources: names and texts, in name order. */
16
+ export declare function runnerSourceHash(files: RunnerSourceFiles): string;
17
+ /**
18
+ * Write the runner sources to `<home>/ios-runner/src/<version>-<hash>/` (the layout of `packages/e2e/native`).
19
+ * Content-keyed and never rewritten in place, so a build still running from an older copy is safe. Written to a
20
+ * sibling temp directory and renamed, so a concurrent warden never sees half a copy; `.ready` marks a complete one.
21
+ */
22
+ export declare function stageRunnerSources(o: {
23
+ home: string;
24
+ version: string;
25
+ loadSources: () => AsyncResult<RunnerSourceFiles>;
26
+ }): AsyncResult<StagedRunner>;
@@ -0,0 +1,126 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import type { Result } from "../../types/src/result";
3
+ import type { WaitReason } from "./queue";
4
+ import type { Platform } from "./types";
5
+ /**
6
+ * The lease event log: an append-only `lease_events` table, one row per thing that happened to a device
7
+ * lease or device. Event type names are `snake_case` throughout (`heartbeat_lost`, `device_unhealthy`).
8
+ * Rows are written inside the same store transaction as the change they record (see `Store`), pruned by
9
+ * `warden gc` after `WARDEN_EVENTS_DAYS` (default 7).
10
+ */
11
+ /** How a granted lease came to be: `leased` is a caller-side lease on a named device (`warden ui`). */
12
+ export type GrantAction = "reuse" | "boot" | "create" | "leased";
13
+ /** Per-type payloads (the JSON `data` column). */
14
+ export type LeaseEventPayloads = {
15
+ /** a `claimDevices` call started */
16
+ requested: {
17
+ platform: Platform;
18
+ profile: string;
19
+ count: number;
20
+ };
21
+ /** a claim started waiting (boot queue or polling for a device): once per claim call, with why */
22
+ queued: {
23
+ platform: Platform;
24
+ profile: string;
25
+ queueId: string;
26
+ reason?: WaitReason;
27
+ };
28
+ /** a device lease was handed to the caller; `waitMs` = time since the claim was requested */
29
+ granted: {
30
+ platform: Platform;
31
+ profile?: string;
32
+ deviceId: string;
33
+ deviceName: string;
34
+ action: GrantAction;
35
+ waitMs: number;
36
+ };
37
+ /** a holder's heartbeat found its lease gone (released or reclaimed by someone else) */
38
+ heartbeat_lost: Record<string, never>;
39
+ /** the lease was deleted by its owner / a release; `heldMs` = time since it was granted */
40
+ released: {
41
+ heldMs: number;
42
+ };
43
+ /** the lease went stale (owner process gone, heartbeat expired) and was reclaimed */
44
+ expired: {
45
+ heldMs: number;
46
+ };
47
+ /** an idle warden device was shut down to make room for the queue head */
48
+ evicted: {
49
+ platform: Platform;
50
+ deviceId: string;
51
+ deviceName: string;
52
+ };
53
+ quarantined: {
54
+ platform: Platform;
55
+ deviceId: string;
56
+ deviceName: string;
57
+ reason?: string;
58
+ };
59
+ unquarantined: {
60
+ platform: Platform;
61
+ deviceId: string;
62
+ };
63
+ /** warden shut a device down: `idle` (gc idle pass) or `release` (a lease ended with the device) */
64
+ shutdown: {
65
+ platform: Platform;
66
+ deviceId: string;
67
+ deviceName?: string;
68
+ cause: "idle" | "release";
69
+ };
70
+ /** the health monitor under a lease read a device bad `consecutive` times in a row (once per episode) */
71
+ device_unhealthy: {
72
+ platform: Platform;
73
+ deviceId: string;
74
+ reason?: string;
75
+ consecutive?: number;
76
+ };
77
+ /** the monitor rebooted the device under the same lease after `attempts` tries, `durationMs` in all */
78
+ device_recovered: {
79
+ platform: Platform;
80
+ deviceId: string;
81
+ attempts?: number;
82
+ durationMs?: number;
83
+ };
84
+ };
85
+ export type LeaseEventType = keyof LeaseEventPayloads;
86
+ export declare const LEASE_EVENT_TYPES: readonly LeaseEventType[];
87
+ type EventBase = {
88
+ at: number;
89
+ leaseId?: string;
90
+ resourceKey?: string;
91
+ ownerKey?: string;
92
+ label?: string;
93
+ claimId?: string;
94
+ };
95
+ /** An event to write; `data` is typed by `type`. */
96
+ export type NewLeaseEvent = {
97
+ [T in LeaseEventType]: EventBase & {
98
+ type: T;
99
+ data: LeaseEventPayloads[T];
100
+ };
101
+ }[LeaseEventType];
102
+ /** A stored event. */
103
+ export type LeaseEvent = NewLeaseEvent & {
104
+ id: number;
105
+ };
106
+ export declare function insertLeaseEvent(db: Database, event: NewLeaseEvent): void;
107
+ export type EventFilter = {
108
+ /** only events at or after this time (ms) */
109
+ since?: number;
110
+ /** exact lease id */
111
+ leaseId?: string;
112
+ /** the owner key (`agent:<sessionId>`, `user:<pid>`, `ci:<runId>`) or any substring of it */
113
+ owner?: string;
114
+ /** only events with an id above this (the `--follow` cursor) */
115
+ afterId?: number;
116
+ };
117
+ /** Events oldest first (by id), matching every given filter. */
118
+ export declare function queryLeaseEvents(db: Database, filter?: EventFilter): LeaseEvent[];
119
+ /** Delete events older than `before` (ms); returns how many. */
120
+ export declare function pruneLeaseEvents(db: Database, before: number): number;
121
+ export declare const DEFAULT_EVENTS_DAYS = 7;
122
+ /** How long events are kept: `WARDEN_EVENTS_DAYS` (a positive integer) else 7 days. */
123
+ export declare function eventsRetentionMs(env: Record<string, string | undefined>): Result<number>;
124
+ /** Labels of leases warden holds for its own housekeeping: never user-visible grants. */
125
+ export declare const INTERNAL_LEASE_LABELS: readonly string[];
126
+ 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;