gridfleet-testkit 0.9.5__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.5 → gridfleet_testkit-0.10.0}/.gitignore +1 -0
  2. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/CHANGELOG.md +26 -0
  3. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/PKG-INFO +9 -9
  4. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/README.md +8 -8
  5. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_android_mobile_screenshot.py +2 -2
  6. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_android_tv_screenshot.py +2 -2
  7. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_firetv_screenshot.py +2 -2
  8. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_ios_simulator_screenshot.py +2 -2
  9. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_roku_screenshot.py +2 -2
  10. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_roku_sideload_screenshot.py +1 -1
  11. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/test_tvos_screenshot.py +2 -2
  12. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/__init__.py +4 -2
  13. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/appium.py +15 -4
  14. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/client.py +11 -0
  15. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/pytest_plugin.py +3 -2
  16. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/pyproject.toml +1 -1
  17. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_appium.py +0 -1
  18. {gridfleet_testkit-0.9.5 → 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.5 → gridfleet_testkit-0.10.0}/uv.lock +22 -22
  21. gridfleet_testkit-0.9.5/tests/test_pytest_plugin_grid_run_id_injection.py +0 -29
  22. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/__init__.py +0 -0
  23. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/_example_helpers.py +0 -0
  24. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/examples/assets/hello-world.zip +0 -0
  25. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/allocation.py +0 -0
  26. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/py.typed +0 -0
  27. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/sessions.py +0 -0
  28. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/gridfleet_testkit/types.py +0 -0
  29. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_allocation.py +0 -0
  30. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_client.py +0 -0
  31. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_client_test_data.py +0 -0
  32. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_docs_contract.py +0 -0
  33. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_driver_agnostic_guard.py +0 -0
  34. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_package_metadata.py +0 -0
  35. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_pytest_plugin_test_data.py +0 -0
  36. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_sessions.py +0 -0
  37. {gridfleet_testkit-0.9.5 → gridfleet_testkit-0.10.0}/tests/test_sessions_resolve_device_handle.py +0 -0
@@ -55,6 +55,7 @@ docs/superpowers/
55
55
  .claude/
56
56
  .repowise/
57
57
  .mcp.json
58
+ graphify-out/
58
59
 
59
60
  # Local-only device-state live test harness (not committed)
60
61
  scripts/state-testing/
@@ -2,6 +2,32 @@
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
+
5
31
  ## [0.9.5](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.4...gridfleet-testkit-v0.9.5) (2026-06-04)
6
32
 
7
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridfleet-testkit
3
- Version: 0.9.5
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
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
68
68
  ## What It Does Not Own
69
69
 
70
70
  - Appium server installation or host-level driver setup
71
- - Selenium Grid lifecycle
71
+ - WebDriver router lifecycle
72
72
  - Device registration, verification, or readiness setup
73
73
  - CI orchestration beyond the documented client helpers
74
74
 
@@ -107,15 +107,15 @@ The package supports Python 3.10 through 3.14.
107
107
 
108
108
  | Variable | Default | Meaning |
109
109
  | --- | --- | --- |
110
- | `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 |
111
111
  | `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
112
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`. |
113
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`. |
114
114
  | `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
115
115
  | `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
116
- | `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. |
117
117
 
118
- 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.
119
119
 
120
120
  ## Pytest Plugin
121
121
 
@@ -149,7 +149,7 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
149
149
 
150
150
  - Creates an Appium session through `GRID_URL`
151
151
  - Injects `gridfleet:testName` with the pytest test name
152
- - Always injects `gridfleet:run_id`, taken from the `GRIDFLEET_RUN_ID` environment variable when set (for example, inside a reserved run), otherwise defaulting to `"free"`
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.
153
153
  - Reports final session status back to `GRIDFLEET_API_URL`
154
154
  - Exposes `device_config` for post-session config lookup using the runtime connection target
155
155
  - Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
@@ -218,7 +218,7 @@ finally:
218
218
 
219
219
  ### Targeting Devices by Tag
220
220
 
221
- 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.
222
222
 
223
223
  ```python
224
224
  @pytest.mark.parametrize(
@@ -254,7 +254,7 @@ The `gridfleet_worker_id` fixture is informational only: it returns the pytest-x
254
254
 
255
255
  ### Reservation Flow
256
256
 
257
- 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.
258
258
 
259
259
  ```python
260
260
  from gridfleet_testkit import GridFleetClient, register_run_cleanup
@@ -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
 
@@ -75,15 +75,15 @@ The package supports Python 3.10 through 3.14.
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
- - Always injects `gridfleet:run_id`, taken from the `GRIDFLEET_RUN_ID` environment variable when set (for example, inside a reserved run), otherwise defaulting to `"free"`
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(
@@ -222,7 +222,7 @@ The `gridfleet_worker_id` fixture is informational only: it returns the pytest-x
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
@@ -386,7 +386,7 @@ Advanced example:
386
386
 
387
387
  The baseline examples share the same flow:
388
388
 
389
- 1. Create a session through Selenium Grid
389
+ 1. Create a session through the WebDriver router
390
390
  2. Print the resolved connection context
391
391
  3. Save a screenshot
392
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.5"
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.5"
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"
@@ -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.5"
139
+ version = "0.10.0"
140
140
  source = { editable = "." }
141
141
  dependencies = [
142
142
  { name = "appium-python-client" },
@@ -454,27 +454,27 @@ wheels = [
454
454
 
455
455
  [[package]]
456
456
  name = "ruff"
457
- version = "0.15.15"
458
- source = { registry = "https://pypi.org/simple" }
459
- sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
460
- wheels = [
461
- { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
462
- { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
463
- { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
464
- { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
465
- { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
466
- { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
467
- { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
468
- { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
469
- { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
470
- { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
471
- { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
472
- { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
473
- { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
474
- { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
475
- { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
476
- { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
477
- { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
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" },
478
478
  ]
479
479
 
480
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"