gridfleet-testkit 0.9.4__tar.gz → 0.10.0__tar.gz

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 (37) hide show
  1. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/.gitignore +10 -2
  2. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/CHANGELOG.md +38 -0
  3. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/PKG-INFO +15 -15
  4. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/README.md +14 -13
  5. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_android_mobile_screenshot.py +2 -2
  6. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_android_tv_screenshot.py +2 -2
  7. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_firetv_screenshot.py +2 -2
  8. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_ios_simulator_screenshot.py +2 -2
  9. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_roku_screenshot.py +2 -2
  10. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_roku_sideload_screenshot.py +1 -1
  11. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/test_tvos_screenshot.py +2 -2
  12. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/__init__.py +4 -2
  13. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/appium.py +15 -4
  14. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/client.py +11 -0
  15. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/pytest_plugin.py +3 -2
  16. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/pyproject.toml +1 -2
  17. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_appium.py +0 -1
  18. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_pytest_plugin.py +0 -1
  19. gridfleet_testkit-0.10.0/tests/test_run_scoped_url.py +131 -0
  20. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/uv.lock +22 -24
  21. gridfleet_testkit-0.9.4/tests/test_pytest_plugin_grid_run_id_injection.py +0 -29
  22. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/__init__.py +0 -0
  23. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/_example_helpers.py +0 -0
  24. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/examples/assets/hello-world.zip +0 -0
  25. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/allocation.py +0 -0
  26. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/py.typed +0 -0
  27. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/sessions.py +0 -0
  28. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/gridfleet_testkit/types.py +0 -0
  29. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_allocation.py +0 -0
  30. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_client.py +0 -0
  31. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_client_test_data.py +0 -0
  32. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_docs_contract.py +0 -0
  33. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_driver_agnostic_guard.py +0 -0
  34. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_package_metadata.py +0 -0
  35. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_pytest_plugin_test_data.py +0 -0
  36. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_sessions.py +0 -0
  37. {gridfleet_testkit-0.9.4 → gridfleet_testkit-0.10.0}/tests/test_sessions_resolve_device_handle.py +0 -0
@@ -42,8 +42,6 @@ dist/
42
42
 
43
43
  # Misc
44
44
  *.log
45
- .idea
46
- testing/screenshots
47
45
  frontend/test-results
48
46
 
49
47
  # Worktrees
@@ -51,3 +49,13 @@ frontend/test-results
51
49
 
52
50
  # Superpowers working docs
53
51
  .superpowers/
52
+ docs/superpowers/
53
+
54
+ # Local tooling
55
+ .claude/
56
+ .repowise/
57
+ .mcp.json
58
+ graphify-out/
59
+
60
+ # Local-only device-state live test harness (not committed)
61
+ scripts/state-testing/
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
4
4
 
5
+ ## [0.10.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.5...gridfleet-testkit-v0.10.0) (2026-06-07)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * **testkit:** bind sessions to runs via the run-scoped grid url
11
+
12
+ ### Features
13
+
14
+ * **testkit:** bind sessions to runs via the run-scoped grid url ([da34f10](https://github.com/quidow/gridfleet/commit/da34f10f095ed74b2733c0746f11632039bd109e))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **testkit:** route the pytest plugin driver fixture through the run-scoped url ([d976c8a](https://github.com/quidow/gridfleet/commit/d976c8afb7c10ade85b3b03fb1c7d40756e952ca))
20
+
21
+
22
+ ### Dependencies
23
+
24
+ * **deps:** bump ruff in /testkit in the python-dependencies group ([#513](https://github.com/quidow/gridfleet/issues/513)) ([f7e8fb9](https://github.com/quidow/gridfleet/commit/f7e8fb92da993155852e328dd2e8e174fe93bba7))
25
+
26
+
27
+ ### Documentation
28
+
29
+ * **docs:** sweep selenium grid references for router architecture ([d086bcb](https://github.com/quidow/gridfleet/commit/d086bcb7c1619fb21cdf5e59499ab8221b18a0e4))
30
+
31
+ ## [0.9.5](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.4...gridfleet-testkit-v0.9.5) (2026-06-04)
32
+
33
+
34
+ ### Dependencies
35
+
36
+ * **deps:** bump ruff in /testkit in the python-dependencies group ([#428](https://github.com/quidow/gridfleet/issues/428)) ([06ba6a7](https://github.com/quidow/gridfleet/commit/06ba6a7936769768436499d7e34f1053f3bf4710))
37
+
38
+
39
+ ### Documentation
40
+
41
+ * **docs:** align all docs with the actual implementation state ([#499](https://github.com/quidow/gridfleet/issues/499)) ([1d7a4ea](https://github.com/quidow/gridfleet/commit/1d7a4ea2afafbd5872856a01a9f73792c9ce5f7f))
42
+
5
43
  ## [0.9.4](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.3...gridfleet-testkit-v0.9.4) (2026-05-24)
6
44
 
7
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridfleet-testkit
3
- Version: 0.9.4
3
+ Version: 0.10.0
4
4
  Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
5
5
  Project-URL: Homepage, https://github.com/quidow/gridfleet
6
6
  Project-URL: Repository, https://github.com/quidow/gridfleet
@@ -27,7 +27,6 @@ Requires-Dist: httpx<1,>=0.27
27
27
  Requires-Dist: pytest<10,>=9.0.3
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: mypy<3,>=1.20.2; extra == 'dev'
30
- Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
31
30
  Requires-Dist: ruff<1,>=0.15.12; extra == 'dev'
32
31
  Description-Content-Type: text/markdown
33
32
 
@@ -69,7 +68,7 @@ Description-Content-Type: text/markdown
69
68
  ## What It Does Not Own
70
69
 
71
70
  - Appium server installation or host-level driver setup
72
- - Selenium Grid lifecycle
71
+ - WebDriver router lifecycle
73
72
  - Device registration, verification, or readiness setup
74
73
  - CI orchestration beyond the documented client helpers
75
74
 
@@ -101,22 +100,22 @@ From a Git checkout or VCS URL that contains this package:
101
100
  uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
102
101
  ```
103
102
 
104
- The package supports Python 3.10 and newer.
103
+ The package supports Python 3.10 through 3.14.
105
104
  `Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
106
105
 
107
106
  ## Environment
108
107
 
109
108
  | Variable | Default | Meaning |
110
109
  | --- | --- | --- |
111
- | `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
110
+ | `GRID_URL` | `http://localhost:4444` | WebDriver router URL used by the pytest Appium fixture |
112
111
  | `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
113
112
  | `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
114
113
  | `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
115
114
  | `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
116
115
  | `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
117
- | `GRIDFLEET_RUN_ID` | `free` | Optional run id injected into Appium capabilities as `gridfleet:run_id`. The pytest plugin sets this automatically when sessions are created inside a reserved run. |
116
+ | `GRIDFLEET_RUN_ID` | unset | Optional run id. When set, drivers are created through the run-scoped grid endpoint `GRID_URL/run/{id}` so sessions land only on devices reserved for the run. Unset = free session on unreserved devices. Set this in the environment that launches pytest (e.g. the run launcher or CI step); the testkit reads it but does not set it. |
118
117
 
119
- The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
118
+ The package assumes a running GridFleet API, a reachable WebDriver router, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
120
119
 
121
120
  ## Pytest Plugin
122
121
 
@@ -150,7 +149,7 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
150
149
 
151
150
  - Creates an Appium session through `GRID_URL`
152
151
  - Injects `gridfleet:testName` with the pytest test name
153
- - Injects `gridfleet:run_id` when a `GRIDFLEET_RUN_ID` environment variable is present (for example, inside a reserved run)
152
+ - Resolves the WebDriver endpoint from `GRIDFLEET_RUN_ID`: run-scoped URL inside a reserved run, bare grid URL otherwise. No GridFleet identity is injected into capabilities.
154
153
  - Reports final session status back to `GRIDFLEET_API_URL`
155
154
  - Exposes `device_config` for post-session config lookup using the runtime connection target
156
155
  - Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
@@ -219,7 +218,7 @@ finally:
219
218
 
220
219
  ### Targeting Devices by Tag
221
220
 
222
- GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
221
+ GridFleet injects device tags into node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so the router's backend allocation can route sessions to devices matching specific tags.
223
222
 
224
223
  ```python
225
224
  @pytest.mark.parametrize(
@@ -251,11 +250,11 @@ When an operator edits device tags, GridFleet marks the device for re-verificati
251
250
 
252
251
  ### Worker Identity
253
252
 
254
- `worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
253
+ The `gridfleet_worker_id` fixture is informational only: it returns the pytest-xdist worker id (normally `gw0`, `gw1`, and so on), or `"controller"` for non-worker processes. It is never transmitted to the manager; use it client-side for local sharding or log correlation. For run attribution, pass the `created_by` argument to `GridFleetClient.reserve_devices` that is the only run-attribution field the reservation request carries.
255
254
 
256
255
  ### Reservation Flow
257
256
 
258
- GridFleet runs are grid-routed: once devices are reserved, the manager tags matching Grid nodes with the run id, and Selenium Grid routes new Appium sessions to those nodes automatically via the `gridfleet:run_id` capability. There are no per-worker claim or release calls.
257
+ GridFleet runs are router-routed: once devices are reserved, the manager tags matching nodes with the run id, and the router routes new Appium sessions to those nodes automatically when they arrive through the run-scoped endpoint (`GRID_URL/run/{run_id}`). There are no per-worker claim or release calls.
259
258
 
260
259
  ```python
261
260
  from gridfleet_testkit import GridFleetClient, register_run_cleanup
@@ -386,11 +385,12 @@ client = GridFleetClient()
386
385
  run = client.reserve_devices(
387
386
  name="my-test-run",
388
387
  requirements=[...],
389
- include=("config", "capabilities"),
388
+ include=("config",),
390
389
  )
391
390
  for device_handle in run["devices"]:
392
391
  allocated = hydrate_allocated_device(device_handle, run_id=run["id"], client=client)
393
- # zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
392
+ # no config follow-up GET; allocated.config populated inline.
393
+ # For capabilities, pass include= on this per-worker hydrate_allocated_device call.
394
394
  ```
395
395
 
396
396
  `device_config` and inline `config` payloads are returned verbatim from the manager. The testkit does not perform client-side secret masking or reveal toggles. Protect device config with manager authentication, operator access control, and your lab's secret-handling policy.
@@ -399,7 +399,7 @@ for device_handle in run["devices"]:
399
399
 
400
400
  `include=` must be a sequence of strings (tuple or list) — order is preserved in the emitted query parameter. Passing a bare string like `include="config"` raises `TypeError` to avoid silently splitting the value into characters.
401
401
 
402
- `hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries or rows returned by `get_device_by_connection_target`.
402
+ `hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries, which carry a top-level `device_id`. A `get_device_by_connection_target` row keys its primary id as `id`, so it must have that field remapped to `device_id` before being passed in.
403
403
 
404
404
  ## Examples
405
405
 
@@ -418,7 +418,7 @@ Advanced example:
418
418
 
419
419
  The baseline examples share the same flow:
420
420
 
421
- 1. Create a session through Selenium Grid
421
+ 1. Create a session through the WebDriver router
422
422
  2. Print the resolved connection context
423
423
  3. Save a screenshot
424
424
  4. Assert that the screenshot file exists and is non-empty
@@ -36,7 +36,7 @@
36
36
  ## What It Does Not Own
37
37
 
38
38
  - Appium server installation or host-level driver setup
39
- - Selenium Grid lifecycle
39
+ - WebDriver router lifecycle
40
40
  - Device registration, verification, or readiness setup
41
41
  - CI orchestration beyond the documented client helpers
42
42
 
@@ -68,22 +68,22 @@ From a Git checkout or VCS URL that contains this package:
68
68
  uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
69
69
  ```
70
70
 
71
- The package supports Python 3.10 and newer.
71
+ The package supports Python 3.10 through 3.14.
72
72
  `Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
73
73
 
74
74
  ## Environment
75
75
 
76
76
  | Variable | Default | Meaning |
77
77
  | --- | --- | --- |
78
- | `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
78
+ | `GRID_URL` | `http://localhost:4444` | WebDriver router URL used by the pytest Appium fixture |
79
79
  | `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
80
80
  | `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
81
81
  | `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
82
82
  | `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
83
83
  | `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
84
- | `GRIDFLEET_RUN_ID` | `free` | Optional run id injected into Appium capabilities as `gridfleet:run_id`. The pytest plugin sets this automatically when sessions are created inside a reserved run. |
84
+ | `GRIDFLEET_RUN_ID` | unset | Optional run id. When set, drivers are created through the run-scoped grid endpoint `GRID_URL/run/{id}` so sessions land only on devices reserved for the run. Unset = free session on unreserved devices. Set this in the environment that launches pytest (e.g. the run launcher or CI step); the testkit reads it but does not set it. |
85
85
 
86
- The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
86
+ The package assumes a running GridFleet API, a reachable WebDriver router, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
87
87
 
88
88
  ## Pytest Plugin
89
89
 
@@ -117,7 +117,7 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
117
117
 
118
118
  - Creates an Appium session through `GRID_URL`
119
119
  - Injects `gridfleet:testName` with the pytest test name
120
- - Injects `gridfleet:run_id` when a `GRIDFLEET_RUN_ID` environment variable is present (for example, inside a reserved run)
120
+ - Resolves the WebDriver endpoint from `GRIDFLEET_RUN_ID`: run-scoped URL inside a reserved run, bare grid URL otherwise. No GridFleet identity is injected into capabilities.
121
121
  - Reports final session status back to `GRIDFLEET_API_URL`
122
122
  - Exposes `device_config` for post-session config lookup using the runtime connection target
123
123
  - Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
@@ -186,7 +186,7 @@ finally:
186
186
 
187
187
  ### Targeting Devices by Tag
188
188
 
189
- GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
189
+ GridFleet injects device tags into node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so the router's backend allocation can route sessions to devices matching specific tags.
190
190
 
191
191
  ```python
192
192
  @pytest.mark.parametrize(
@@ -218,11 +218,11 @@ When an operator edits device tags, GridFleet marks the device for re-verificati
218
218
 
219
219
  ### Worker Identity
220
220
 
221
- `worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
221
+ The `gridfleet_worker_id` fixture is informational only: it returns the pytest-xdist worker id (normally `gw0`, `gw1`, and so on), or `"controller"` for non-worker processes. It is never transmitted to the manager; use it client-side for local sharding or log correlation. For run attribution, pass the `created_by` argument to `GridFleetClient.reserve_devices` that is the only run-attribution field the reservation request carries.
222
222
 
223
223
  ### Reservation Flow
224
224
 
225
- GridFleet runs are grid-routed: once devices are reserved, the manager tags matching Grid nodes with the run id, and Selenium Grid routes new Appium sessions to those nodes automatically via the `gridfleet:run_id` capability. There are no per-worker claim or release calls.
225
+ GridFleet runs are router-routed: once devices are reserved, the manager tags matching nodes with the run id, and the router routes new Appium sessions to those nodes automatically when they arrive through the run-scoped endpoint (`GRID_URL/run/{run_id}`). There are no per-worker claim or release calls.
226
226
 
227
227
  ```python
228
228
  from gridfleet_testkit import GridFleetClient, register_run_cleanup
@@ -353,11 +353,12 @@ client = GridFleetClient()
353
353
  run = client.reserve_devices(
354
354
  name="my-test-run",
355
355
  requirements=[...],
356
- include=("config", "capabilities"),
356
+ include=("config",),
357
357
  )
358
358
  for device_handle in run["devices"]:
359
359
  allocated = hydrate_allocated_device(device_handle, run_id=run["id"], client=client)
360
- # zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
360
+ # no config follow-up GET; allocated.config populated inline.
361
+ # For capabilities, pass include= on this per-worker hydrate_allocated_device call.
361
362
  ```
362
363
 
363
364
  `device_config` and inline `config` payloads are returned verbatim from the manager. The testkit does not perform client-side secret masking or reveal toggles. Protect device config with manager authentication, operator access control, and your lab's secret-handling policy.
@@ -366,7 +367,7 @@ for device_handle in run["devices"]:
366
367
 
367
368
  `include=` must be a sequence of strings (tuple or list) — order is preserved in the emitted query parameter. Passing a bare string like `include="config"` raises `TypeError` to avoid silently splitting the value into characters.
368
369
 
369
- `hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries or rows returned by `get_device_by_connection_target`.
370
+ `hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries, which carry a top-level `device_id`. A `get_device_by_connection_target` row keys its primary id as `id`, so it must have that field remapped to `device_id` before being passed in.
370
371
 
371
372
  ## Examples
372
373
 
@@ -385,7 +386,7 @@ Advanced example:
385
386
 
386
387
  The baseline examples share the same flow:
387
388
 
388
- 1. Create a session through Selenium Grid
389
+ 1. Create a session through the WebDriver router
389
390
  2. Print the resolved connection context
390
391
  3. Save a screenshot
391
392
  4. Assert that the screenshot file exists and is non-empty
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to an Android mobile device through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to an Android mobile device through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - An Android mobile device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium-Python-Client installed (`uv pip install -e ./testkit`)
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to an Android TV device through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to an Android TV device through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - An Android TV device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium-Python-Client installed (`uv pip install -e ./testkit`)
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to a Fire TV device through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to a Fire TV device through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - A Fire TV device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium-Python-Client installed (`uv pip install -e ./testkit`)
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to an iOS simulator through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to an iOS simulator through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - An iOS simulator registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium with the XCUITest driver installed (`appium driver install xcuitest`)
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to a Roku device through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to a Roku device through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - A Roku device registered with Roku dev credentials in device config
7
7
  - Appium with the Roku driver installed (`appium driver install roku`)
8
8
  - The supported GridFleet testkit installed
@@ -2,7 +2,7 @@
2
2
  Manual advanced example: connect to a Roku device, sideload a sample app, and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - A Roku device registered with Roku dev credentials in device config
7
7
  - Appium with the Roku driver installed (`appium driver install roku`)
8
8
  - The supported GridFleet testkit installed
@@ -1,8 +1,8 @@
1
1
  """
2
- Manual baseline example: connect to a tvOS real device through Selenium Grid and take a screenshot.
2
+ Manual baseline example: connect to a tvOS real device through the WebDriver router and take a screenshot.
3
3
 
4
4
  Requires:
5
- - Selenium Grid hub running on localhost:4444
5
+ - WebDriver router running on localhost:4444
6
6
  - A tvOS real device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium with the XCUITest driver installed (`appium driver install xcuitest`)
@@ -6,7 +6,7 @@ the live Appium-side config can use `client.get_device_config(connection_target)
6
6
 
7
7
  Environment variables read by the client:
8
8
 
9
- - GRID_URL: Selenium Grid URL used by Appium helper defaults.
9
+ - GRID_URL: WebDriver router URL used by Appium helper defaults.
10
10
  - GRIDFLEET_API_URL: GridFleet manager API base URL.
11
11
  - GRIDFLEET_TESTKIT_USERNAME: optional Basic auth username.
12
12
  - GRIDFLEET_TESTKIT_PASSWORD: optional Basic auth password.
@@ -41,13 +41,14 @@ from .client import (
41
41
  _default_api_url,
42
42
  _default_grid_url,
43
43
  register_run_cleanup,
44
+ run_grid_url,
44
45
  )
45
46
  from .sessions import build_error_session_payload, resolve_device_handle_from_driver
46
47
 
47
48
  try:
48
49
  __version__ = version("gridfleet-testkit")
49
50
  except PackageNotFoundError:
50
- __version__ = "0.9.4"
51
+ __version__ = "0.10.0"
51
52
 
52
53
  __all__ = [
53
54
  "GRIDFLEET_API_URL",
@@ -72,6 +73,7 @@ __all__ = [
72
73
  "hydrate_allocated_device_from_driver",
73
74
  "register_run_cleanup",
74
75
  "resolve_device_handle_from_driver",
76
+ "run_grid_url",
75
77
  ]
76
78
 
77
79
 
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast
8
8
  from appium import webdriver
9
9
  from appium.options.common import AppiumOptions
10
10
 
11
- from .client import GridFleetClient, _default_grid_url
11
+ from .client import GridFleetClient, _default_grid_url, run_grid_url
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Callable, Mapping
@@ -102,7 +102,6 @@ def build_appium_options(
102
102
  ) -> AppiumOptions:
103
103
  """Build Appium options from driver-pack catalog platform metadata."""
104
104
  params = dict(capabilities or {})
105
- params.setdefault("gridfleet:run_id", os.environ.get("GRIDFLEET_RUN_ID", "free"))
106
105
  explicit_platform_name = params.get("platformName")
107
106
  if explicit_platform_name is not None and (pack_id is not None or platform_id is not None):
108
107
  raise ValueError("Use either pack_id/platform_id or the raw platformName capability, not both.")
@@ -126,6 +125,18 @@ def build_appium_options(
126
125
  return options
127
126
 
128
127
 
128
+ def _resolve_grid_url(grid_url: str | None) -> str:
129
+ """Executor resolution: explicit URL wins; GRIDFLEET_RUN_ID (set externally
130
+ by the run launcher or CI before pytest starts) composes the run-scoped
131
+ endpoint; otherwise the bare grid URL — an explicit free session."""
132
+ if grid_url is not None:
133
+ return grid_url
134
+ run_id = os.environ.get("GRIDFLEET_RUN_ID")
135
+ if run_id:
136
+ return run_grid_url(run_id)
137
+ return _default_grid_url()
138
+
139
+
129
140
  def create_appium_driver(
130
141
  *,
131
142
  pack_id: str | None = None,
@@ -135,7 +146,7 @@ def create_appium_driver(
135
146
  grid_url: str | None = None,
136
147
  catalog_client: object | None = None,
137
148
  ) -> WebDriver:
138
- """Create an Appium remote driver through Selenium Grid."""
149
+ """Create an Appium remote driver through the WebDriver router."""
139
150
  options = build_appium_options(
140
151
  pack_id=pack_id,
141
152
  platform_id=platform_id,
@@ -143,7 +154,7 @@ def create_appium_driver(
143
154
  test_name=test_name,
144
155
  catalog_client=catalog_client,
145
156
  )
146
- return webdriver.Remote(grid_url or _default_grid_url(), options=options)
157
+ return webdriver.Remote(_resolve_grid_url(grid_url), options=options)
147
158
 
148
159
 
149
160
  def get_connection_target_from_driver(driver: WebDriver) -> str:
@@ -31,6 +31,17 @@ def _default_grid_url() -> str:
31
31
  return os.getenv("GRID_URL", DEFAULT_GRID_URL)
32
32
 
33
33
 
34
+ def run_grid_url(run_id: str, *, base: str | None = None) -> str:
35
+ """Run-scoped WebDriver endpoint for *run_id* (``{base}/run/{run_id}``).
36
+
37
+ Sessions created through it are admitted only to devices reserved for the
38
+ run; free sessions use the bare grid URL. Replaces the retired
39
+ ``gridfleet:run_id`` capability.
40
+ """
41
+ root = (base or _default_grid_url()).rstrip("/")
42
+ return f"{root}/run/{run_id}"
43
+
44
+
34
45
  def _default_api_url() -> str:
35
46
  return os.getenv("GRIDFLEET_API_URL", DEFAULT_GRIDFLEET_API_URL)
36
47
 
@@ -9,6 +9,7 @@ import pytest
9
9
  from appium import webdriver
10
10
 
11
11
  from .appium import (
12
+ _resolve_grid_url,
12
13
  build_appium_options,
13
14
  get_device_config_for_driver,
14
15
  get_device_test_data_for_driver,
@@ -72,7 +73,7 @@ def appium_driver(
72
73
  gridfleet_client: GridFleetClient,
73
74
  ) -> Generator[WebDriver, None, None]:
74
75
  """
75
- Create an Appium Remote driver through the Selenium Grid.
76
+ Create an Appium Remote driver through the WebDriver router.
76
77
 
77
78
  Parametrize with a dict of pack/catalog selection plus capabilities:
78
79
  @pytest.mark.parametrize(
@@ -84,7 +85,7 @@ def appium_driver(
84
85
  options = _build_driver_options(request, gridfleet_client)
85
86
 
86
87
  try:
87
- driver = webdriver.Remote(_default_grid_url(), options=options)
88
+ driver = webdriver.Remote(_resolve_grid_url(None), options=options)
88
89
  except Exception as exc:
89
90
  # Driver creation failed before a Grid session was established (e.g.
90
91
  # SessionNotCreatedException). Register a device-less error session so the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gridfleet-testkit"
7
- version = "0.9.4"
7
+ version = "0.10.0"
8
8
  description = "Supported pytest and run-orchestration helpers for GridFleet integrations"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -39,7 +39,6 @@ Security = "https://github.com/quidow/gridfleet/security/advisories/new"
39
39
 
40
40
  [project.optional-dependencies]
41
41
  dev = [
42
- "pytest>=9.0.3,<10",
43
42
  "mypy>=1.20.2,<3",
44
43
  "ruff>=0.15.12,<1",
45
44
  ]
@@ -166,7 +166,6 @@ def test_create_appium_driver_uses_factory_options(monkeypatch: pytest.MonkeyPat
166
166
  "platformName": "Android",
167
167
  "appium:platform": "firetv_real",
168
168
  "appium:automationName": "UiAutomator2",
169
- "gridfleet:run_id": "free",
170
169
  "gridfleet:testName": "manual-smoke",
171
170
  },
172
171
  )
@@ -286,7 +286,6 @@ def test_appium_driver_setup_failure_registers_device_less_error_session(monkeyp
286
286
  "appium:connection_type": "network",
287
287
  "appium:appPackage": "io.appium.android.apis",
288
288
  "platformName": "Android",
289
- "gridfleet:run_id": "free",
290
289
  "gridfleet:testName": "test_broken",
291
290
  "appium:platform": "android_mobile",
292
291
  }
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import types
4
+ from typing import TYPE_CHECKING
5
+
6
+ import gridfleet_testkit.appium as appium_mod
7
+ from gridfleet_testkit import pytest_plugin, run_grid_url
8
+ from gridfleet_testkit.appium import _resolve_grid_url, build_appium_options
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Iterator
12
+
13
+ import pytest
14
+
15
+ RID = "0c8c057f-3ec1-4b9c-9d2e-9f3a86a2c001"
16
+
17
+
18
+ def test_run_grid_url_composes_from_explicit_base() -> None:
19
+ assert run_grid_url(RID, base="http://router:4444/") == f"http://router:4444/run/{RID}"
20
+
21
+
22
+ def test_run_grid_url_defaults_base_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
23
+ monkeypatch.setenv("GRID_URL", "http://lab-router:4444")
24
+ assert run_grid_url(RID) == f"http://lab-router:4444/run/{RID}"
25
+
26
+
27
+ def test_resolve_grid_url_explicit_wins(monkeypatch: pytest.MonkeyPatch) -> None:
28
+ monkeypatch.setenv("GRIDFLEET_RUN_ID", RID)
29
+ assert _resolve_grid_url("http://explicit:4444") == "http://explicit:4444"
30
+
31
+
32
+ def test_resolve_grid_url_uses_run_env(monkeypatch: pytest.MonkeyPatch) -> None:
33
+ monkeypatch.setenv("GRID_URL", "http://router:4444")
34
+ monkeypatch.setenv("GRIDFLEET_RUN_ID", RID)
35
+ assert _resolve_grid_url(None) == f"http://router:4444/run/{RID}"
36
+
37
+
38
+ def test_resolve_grid_url_unset_env_is_free_session(monkeypatch: pytest.MonkeyPatch) -> None:
39
+ monkeypatch.setenv("GRID_URL", "http://router:4444")
40
+ monkeypatch.delenv("GRIDFLEET_RUN_ID", raising=False)
41
+ assert _resolve_grid_url(None) == "http://router:4444"
42
+
43
+
44
+ def test_no_run_id_capability_injected(monkeypatch: pytest.MonkeyPatch) -> None:
45
+ """The cap-era contract is dead: no gridfleet:run_id regardless of env."""
46
+ monkeypatch.setenv("GRIDFLEET_RUN_ID", RID)
47
+ options = build_appium_options(capabilities={"platformName": "Android"})
48
+ assert "gridfleet:run_id" not in dict(options.to_capabilities())
49
+
50
+
51
+ # --- pytest plugin fixture URL routing ---
52
+
53
+
54
+ class _FakeOptions:
55
+ def __init__(self) -> None:
56
+ self.platform_name: str | None = None
57
+ self.capabilities: dict[str, object] = {}
58
+
59
+ def set_capability(self, key: str, value: object) -> None:
60
+ self.capabilities[key] = value
61
+
62
+
63
+ class _FakeDriver:
64
+ def __init__(self) -> None:
65
+ self.session_id = "sess-x"
66
+ self.capabilities: dict[str, object] = {}
67
+
68
+ def quit(self) -> None:
69
+ pass
70
+
71
+
72
+ class _FakeClient:
73
+ def get_driver_pack_catalog(self) -> dict[str, object]:
74
+ return {"packs": []}
75
+
76
+ def register_session_from_driver(self, driver: object, **kwargs: object) -> dict[str, object]:
77
+ return {"ok": True}
78
+
79
+ def register_session(self, **kwargs: object) -> dict[str, object]:
80
+ return {"ok": True}
81
+
82
+ def update_session_status(self, session_id: str, status: str, *, suppress_errors: bool = True) -> dict[str, object]:
83
+ return {"ok": True}
84
+
85
+
86
+ def _make_plugin_generator(monkeypatch: pytest.MonkeyPatch) -> tuple[list[tuple[str, object]], Iterator[object]]:
87
+ """Return (captured_calls, generator) after installing a minimal webdriver.Remote spy."""
88
+ captured: list[tuple[str, object]] = []
89
+
90
+ def fake_remote(url: str, *, options: object) -> _FakeDriver:
91
+ captured.append((url, options))
92
+ return _FakeDriver()
93
+
94
+ monkeypatch.setattr(appium_mod, "AppiumOptions", _FakeOptions)
95
+ # String target: `pytest_plugin.webdriver` is a transitive module attribute
96
+ # mypy strict (no_implicit_reexport) refuses to access statically.
97
+ monkeypatch.setattr("gridfleet_testkit.pytest_plugin.webdriver.Remote", fake_remote)
98
+
99
+ request = types.SimpleNamespace(
100
+ param={"platformName": "Android"},
101
+ node=types.SimpleNamespace(name="test_plugin_url"),
102
+ )
103
+ # pytest wraps fixtures in FixtureFunctionDefinition; __wrapped__ exists at
104
+ # runtime but not in pytest's stubs, so reach it via getattr.
105
+ fixture_fn = getattr(pytest_plugin.appium_driver, "__wrapped__") # noqa: B009
106
+ gen: Iterator[object] = fixture_fn(request, _FakeClient())
107
+ return captured, gen
108
+
109
+
110
+ def test_plugin_fixture_uses_run_scoped_url_when_run_id_set(monkeypatch: pytest.MonkeyPatch) -> None:
111
+ """When GRIDFLEET_RUN_ID is set, the plugin driver connects to GRID_URL/run/{id}."""
112
+ monkeypatch.setenv("GRID_URL", "http://router:4444")
113
+ monkeypatch.setenv("GRIDFLEET_RUN_ID", RID)
114
+
115
+ captured, gen = _make_plugin_generator(monkeypatch)
116
+ next(gen)
117
+
118
+ assert len(captured) == 1
119
+ assert captured[0][0] == f"http://router:4444/run/{RID}"
120
+
121
+
122
+ def test_plugin_fixture_uses_bare_url_when_run_id_unset(monkeypatch: pytest.MonkeyPatch) -> None:
123
+ """When GRIDFLEET_RUN_ID is absent, the plugin driver connects to the bare GRID_URL."""
124
+ monkeypatch.setenv("GRID_URL", "http://router:4444")
125
+ monkeypatch.delenv("GRIDFLEET_RUN_ID", raising=False)
126
+
127
+ captured, gen = _make_plugin_generator(monkeypatch)
128
+ next(gen)
129
+
130
+ assert len(captured) == 1
131
+ assert captured[0][0] == "http://router:4444"
@@ -136,7 +136,7 @@ wheels = [
136
136
 
137
137
  [[package]]
138
138
  name = "gridfleet-testkit"
139
- version = "0.9.4"
139
+ version = "0.10.0"
140
140
  source = { editable = "." }
141
141
  dependencies = [
142
142
  { name = "appium-python-client" },
@@ -147,7 +147,6 @@ dependencies = [
147
147
  [package.optional-dependencies]
148
148
  dev = [
149
149
  { name = "mypy" },
150
- { name = "pytest" },
151
150
  { name = "ruff" },
152
151
  ]
153
152
 
@@ -157,7 +156,6 @@ requires-dist = [
157
156
  { name = "httpx", specifier = ">=0.27,<1" },
158
157
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.20.2,<3" },
159
158
  { name = "pytest", specifier = ">=9.0.3,<10" },
160
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3,<10" },
161
159
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.12,<1" },
162
160
  ]
163
161
  provides-extras = ["dev"]
@@ -456,27 +454,27 @@ wheels = [
456
454
 
457
455
  [[package]]
458
456
  name = "ruff"
459
- version = "0.15.14"
460
- source = { registry = "https://pypi.org/simple" }
461
- sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
462
- wheels = [
463
- { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
464
- { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
465
- { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
466
- { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
467
- { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
468
- { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
469
- { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
470
- { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
471
- { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
472
- { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
473
- { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
474
- { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
475
- { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
476
- { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
477
- { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
478
- { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
479
- { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
457
+ version = "0.15.16"
458
+ source = { registry = "https://pypi.org/simple" }
459
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
460
+ wheels = [
461
+ { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
462
+ { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
463
+ { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
464
+ { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
465
+ { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
466
+ { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
467
+ { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
468
+ { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
469
+ { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
470
+ { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
471
+ { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
472
+ { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
473
+ { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
474
+ { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
475
+ { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
476
+ { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
477
+ { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
480
478
  ]
481
479
 
482
480
  [[package]]
@@ -1,29 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from gridfleet_testkit.appium import build_appium_options
6
-
7
- if TYPE_CHECKING:
8
- import pytest
9
- from appium.options.common import AppiumOptions
10
-
11
-
12
- def _caps(options: AppiumOptions) -> dict[str, object]:
13
- return dict(options.to_capabilities())
14
-
15
-
16
- def test_injects_free_when_no_run_id_env(monkeypatch: pytest.MonkeyPatch) -> None:
17
- monkeypatch.delenv("GRIDFLEET_RUN_ID", raising=False)
18
-
19
- options = build_appium_options(capabilities={"platformName": "Android"})
20
-
21
- assert _caps(options)["gridfleet:run_id"] == "free"
22
-
23
-
24
- def test_injects_run_id_env(monkeypatch: pytest.MonkeyPatch) -> None:
25
- monkeypatch.setenv("GRIDFLEET_RUN_ID", "run-123")
26
-
27
- options = build_appium_options(capabilities={"platformName": "Android"})
28
-
29
- assert _caps(options)["gridfleet:run_id"] == "run-123"