9.1 KiB
Context
driver/base.py::Driver is the driver-independent ABC (connect/disconnect/screenshot/tap/swipe/input/launch/terminate/tree/home/lock/unlock). driver/wda_driver.py::WDADriver is the only implementation today: it wraps the Appium Python Client's XCUITestOptions, builds a webdriver.Remote session against an Appium server, and wraps every operation's exceptions into core.errors.DriverError/DeviceOfflineError. driver/registry.py::SUPPORTED_DRIVER_TYPES maps a driver_type string (today only "wda") to a builder that strips out the driver's own config fields from a connection_info dict and routes the rest into extra_capabilities.
openspec/specs/driver-registry/spec.md already documents this mechanism as driver-type-agnostic: "Adding a driver type requires no changes outside the driver layer." Grepping api/console.py and api/rest.py confirms neither contains a "wda" literal — the API layer resolves driver_type purely through the registry, so this change is expected to be additive to driver/ alone.
This is exactly the case the project's Hexagonal + DDD governance decision calls out: core/driver/device/tools must stay free of LLM/HTTP-framework/MCP dependencies, and every new driver's design must state how it preserves that boundary. docs/MACOS_IPHONE_SETUP.md §1 already documents Android as the known, not-yet-registered gap this change closes.
Goals / Non-Goals
Goals:
- Add a second real
Driverimplementation (AndroidDriver, Appium UiAutomator2) proving the registry extension point generalizes beyond WDA. - Keep the change additive: no edits to
api/,device/,tools/, orruntime/. - Bring the new driver's test coverage above the existing WDA bar (mocked unit tests, not just a hardware-gated integration test).
Non-Goals:
- A full Android SDK/adb/real-device setup guide (
docs/ANDROID_SETUP.mdor equivalent) — deferred to a follow-up change once the driver can be validated against real hardware (user-confirmed scope decision). - Espresso driver support — UiAutomator2 only, mirroring WDA's XCUITest-only scope.
- Renaming
APEX_WDA_*env vars — an unrelated, already-documented follow-up item indocs/MACOS_IPHONE_SETUP.md§12. - Any change to
Driver's abstract interface — the existing method set is sufficient; Android does not need new capabilities the ABC doesn't already express.
Decisions
- Registry key is
"uiautomator2", not"android". The existing key"wda"names the automation backend (WebDriverAgent), not the platform ("ios"). For the two supported driver types to stay consistent, Android's key should likewise name its backend — Appium's UiAutomator2 driver — not its platform. Rejected alternative:"android", which would break that symmetry and would also be ambiguous if Espresso support is ever added later (both would be "android"). AndroidDriverConfigmirrorsWDADriverConfigfield-for-field where an Android equivalent exists:server_url(same defaulthttp://127.0.0.1:4723— one Appium server can host sessions for both platforms),platform_name="Android",automation_name="UiAutomator2",device_name,udid(adb serial, selects among multiple connected devices),no_reset,extra_capabilities.wda_local_port(WDA's per-session port-isolation capability for parallel devices) has a direct UiAutomator2 analog — a system-port capability serving the same purpose — carried over assystem_port. Rejected alternative: a from-scratch config shape — rejected because the parity makes both drivers predictable to configure from the sameconnection_infodict shape the registry already handles generically.- Same error-wrapping pattern as
WDADriver: every method's Appium/network exception is caught and re-raised as the existingDriverError, withDeviceOfflineErrorreserved for connect failures and pre-connect calls (via the same_require_client()guard pattern). Rejected alternative: introducing Android-specific error types — rejected becausecore/errors.py's existing hierarchy is already driver-agnostic and callers above the driver layer must not need to know which concrete driver raised. - Exact Appium UiAutomator2 mobile-command names/parameters for gesture-based methods (
tap,swipe,home) are confirmed against the installedAppium-Python-Clientsource/docs at implementation time, not guessed here. WDA'stap/swipe/homeuse WDA-specificmobile:command names (mobile: tap,mobile: dragFromToForDuration,mobile: pressButton) that do not carry over verbatim to UiAutomator2, which exposes its own gesture command set (e.g. click/drag/swipe gesture commands) with different parameter shapes. Implementation must read the installed driver's actual capability before writing each method body — this is called out as its own task rather than assumed.screenshot/tree/input/launch/terminate/lock/unlockmap to the same cross-platform Selenium/Appium client methods WDA already uses (get_screenshot_as_png,page_source,switch_to.active_element.send_keys,activate_app,terminate_app,lock,unlock) and need no research. driver-registryspec gets a new scenario, not a new capability file. Nowda-drivercapability spec exists today — the registry mechanism is spec'd once, generically, and individual driver behavior is not separately spec'd. Adding anandroid-drivercapability would break that precedent for no benefit; instead,driver-registry's existing "Building a factory for a known driver type" requirement gets a second scenario fordriver_type="uiautomator2", mirroring the existing"wda"scenario, so the spec's example coverage stays symmetric across both real driver types.- Test coverage exceeds current WDA parity on purpose.
WDADrivertoday has zero mocked unit tests — onlytests/test_wda_integration.py, hardware-gated and skipped withoutAPEX_WDA_*env vars. ForAndroidDriver, add mocked unit tests (mockappium.webdriver.Remote) covering connect failure →DeviceOfflineError, pre-connect calls →DeviceOfflineError, and operation exceptions →DriverError, in addition to an equivalent hardware-gatedtests/test_android_integration.py. This is a deliberate quality bar increase, not scope creep — it costs nothing extra in production code and closes a gap the WDA driver has always had. - No new setup documentation in this change. Writing an accurate, detailed Android SDK/adb/real-device guide (parallel to
docs/MACOS_IPHONE_SETUP.md's 12 sections) without access to real hardware to validate each step would mean inventing untested instructions — the existing iOS doc reads as having been validated against a real device.docs/MACOS_IPHONE_SETUP.md§1 gets only a factual correction (Android driver is now registered); a full setup guide is explicit follow-up work once real-device validation is possible.
Risks / Trade-offs
- [Appium UiAutomator2 mobile-command names for tap/swipe/home are not yet confirmed] → Mitigation: dedicated implementation task to read the installed
Appium-Python-ClientUiAutomator2 driver source/docs before writing these three method bodies; not treated as settled by this design doc. - [No real Android device or emulator available during this change to exercise
connect()/screenshot()end-to-end] → Mitigation: mocked unit tests cover the error-handling contract; the integration test is env-var gated and simply skips until real hardware is available, exactly mirroringWDADriver's current state — no regression versus today's validation depth. - [
driver-registryspec now carries two platform-specific example scenarios under one requirement] → Mitigation: accepted; this is the intended pattern for a third driver type in the future, not a maintenance burden.
Migration Plan
- Add
driver/android_driver.py(AndroidDriverConfig,AndroidDriver(Driver)), verifying exact UiAutomator2 mobile-command names/parameters against the installed Appium client before implementingtap/swipe/home. - Add
build_android_driver_factoryand registerSUPPORTED_DRIVER_TYPES["uiautomator2"]indriver/registry.py. - Add mocked unit tests for
AndroidDriver(connect failure, pre-connect calls, per-method exception wrapping). - Add
tests/test_android_integration.py, mirroringtests/test_wda_integration.py's env-var-gated structure. - Add the
driver-registryspec delta scenario fordriver_type="uiautomator2". - Correct
docs/MACOS_IPHONE_SETUP.md§1's now-outdated "Android not registered" statement. - Run the full non-integration test suite (
uv run --all-packages pytest -m "not integration") and confirm no regressions.
Rollback: everything is additive (one new driver module, one new registry entry, new test files, a one-paragraph doc correction). No data/schema migration. Rollback is deleting the new files and reverting the doc line.
Open Questions
- Exact UiAutomator2 mobile-command names/parameters for
tap/swipe/home— resolved during implementation (task-level, not a proposal-level blocker). - Exact capability key name for the system-port equivalent (
systemPortor similar) — confirmed againstUiAutomator2Optionsduring implementation.