chore(openspec): add android-driver proposal
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-13
|
||||
@@ -0,0 +1,53 @@
|
||||
## 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 `Driver` implementation (`AndroidDriver`, Appium UiAutomator2) proving the registry extension point generalizes beyond WDA.
|
||||
- Keep the change additive: no edits to `api/`, `device/`, `tools/`, or `runtime/`.
|
||||
- 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.md` or 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 in `docs/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").
|
||||
- **`AndroidDriverConfig` mirrors `WDADriverConfig` field-for-field where an Android equivalent exists**: `server_url` (same default `http://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 as `system_port`. Rejected alternative: a from-scratch config shape — rejected because the parity makes both drivers predictable to configure from the same `connection_info` dict 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 existing `DriverError`, with `DeviceOfflineError` reserved for connect failures and pre-connect calls (via the same `_require_client()` guard pattern). Rejected alternative: introducing Android-specific error types — rejected because `core/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 installed `Appium-Python-Client` source/docs at implementation time, not guessed here.** WDA's `tap`/`swipe`/`home` use WDA-specific `mobile:` 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`/`unlock` map 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-registry` spec gets a new scenario, not a new capability file.** No `wda-driver` capability spec exists today — the registry mechanism is spec'd once, generically, and individual driver behavior is not separately spec'd. Adding an `android-driver` capability 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 for `driver_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.** `WDADriver` today has zero mocked unit tests — only `tests/test_wda_integration.py`, hardware-gated and skipped without `APEX_WDA_*` env vars. For `AndroidDriver`, add mocked unit tests (mock `appium.webdriver.Remote`) covering connect failure → `DeviceOfflineError`, pre-connect calls → `DeviceOfflineError`, and operation exceptions → `DriverError`, in addition to an equivalent hardware-gated `tests/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-Client` UiAutomator2 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 mirroring `WDADriver`'s current state — no regression versus today's validation depth.
|
||||
- [`driver-registry` spec 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
|
||||
|
||||
1. Add `driver/android_driver.py` (`AndroidDriverConfig`, `AndroidDriver(Driver)`), verifying exact UiAutomator2 mobile-command names/parameters against the installed Appium client before implementing `tap`/`swipe`/`home`.
|
||||
2. Add `build_android_driver_factory` and register `SUPPORTED_DRIVER_TYPES["uiautomator2"]` in `driver/registry.py`.
|
||||
3. Add mocked unit tests for `AndroidDriver` (connect failure, pre-connect calls, per-method exception wrapping).
|
||||
4. Add `tests/test_android_integration.py`, mirroring `tests/test_wda_integration.py`'s env-var-gated structure.
|
||||
5. Add the `driver-registry` spec delta scenario for `driver_type="uiautomator2"`.
|
||||
6. Correct `docs/MACOS_IPHONE_SETUP.md` §1's now-outdated "Android not registered" statement.
|
||||
7. 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 (`systemPort` or similar) — confirmed against `UiAutomator2Options` during implementation.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
`docs/MACOS_IPHONE_SETUP.md` 明确记录了当前的架构缺口:"当前仓库只内置了 `wda` Driver……Android 只是架构上的未来目标,当前 `driver/registry.py` 没有注册 Android Driver,因此仅安装 Android SDK/ADB 还不能让本项目控制 Android 手机。" Hexagonal + DDD 分层治理架构(`driver` 层的 Driver Registry 扩展点模式)从一开始就是为了让新增一个设备平台只需要在 `driver/` 包内添加代码,`driver/wda_driver.py` 已经把这条路径验证了一遍。现在补上 Android 驱动是把这个既定扩展点落地到第二个真实平台,不是新设计。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `driver/android_driver.py`:`AndroidDriverConfig`(dataclass)+ `AndroidDriver(Driver)`,通过 Appium Python Client 的 UiAutomator2 driver 连接 Android 设备(真机或模拟器均可,Appium/adb 本身对两者透明),实现 `driver/base.py::Driver` 抽象基类的全部方法(connect/disconnect/screenshot/tap/swipe/input/launch/terminate/tree/home/lock/unlock)。
|
||||
- `driver/registry.py`:新增 `build_android_driver_factory`,注册到 `SUPPORTED_DRIVER_TYPES["uiautomator2"]`(key 用自动化后端名而非平台名,与现有 `"wda"` 的命名惯例对称)。
|
||||
- 新增 `AndroidDriver` 的 mock 单元测试(mock `appium.webdriver.Remote`),覆盖连接失败、未连接时调用、各操作异常包装为 `DriverError`/`DeviceOfflineError` 的路径——这是比 `WDADriver` 现有测试覆盖更完整的增量,`WDADriver` 目前只有一个真机门控的集成测试。
|
||||
- 新增 `tests/test_android_integration.py`:结构镜像 `tests/test_wda_integration.py`,`@pytest.mark.integration` 门控,依赖 `APEX_ANDROID_SERVER_URL`/`APEX_ANDROID_UDID`/`APEX_ANDROID_DEVICE_NAME` 环境变量,无真机环境时 skip。
|
||||
- 订正 `docs/MACOS_IPHONE_SETUP.md` 第 1 节中"Android 未注册"的过时表述,改为准确描述 Android 驱动已注册、真机安装手册留待后续变更(不在本次新增)。
|
||||
- 不引入新依赖:根 `pyproject.toml` 已声明 `Appium-Python-Client>=5.1.1`,`.venv` 已安装 `appium.options.android.uiautomator2`。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
(无。沿用现有惯例:单个驱动实现的具体行为不单独建 spec capability——`driver/wda_driver.py` 落地时也没有为它建一个 `wda-driver` spec,`openspec/specs/` 里只有 `driver-registry` 这一个与驱动相关的 capability,负责 registry 机制本身。为 Android 单独建一个对称的 capability 会与既有惯例不一致。)
|
||||
|
||||
### Modified Capabilities
|
||||
- `driver-registry`:为"Building a factory for a known driver type"这条既有 Requirement 补一个 `driver_type="uiautomator2"` 的 Scenario,与既有 `driver_type="wda"` 的示例场景对称,证明这条已声明为 driver_type 无关的机制对第二个真实驱动类型同样成立。不新增强约束,只是示例覆盖的完整性。
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增代码**:`driver/android_driver.py`;`tests/test_android_integration.py`;新增覆盖 `AndroidDriver` 的 mock 单元测试文件。
|
||||
- **修改代码**:`driver/registry.py`(新增一个注册项,纯增量,不改动现有 `"wda"` 行为);`docs/MACOS_IPHONE_SETUP.md`(订正第 1 节一句过时表述)。
|
||||
- **依赖**:无新增,复用已声明的 `Appium-Python-Client`。
|
||||
- **不涉及**:`api/`、`device/`、`tools/`、`runtime/` 任一层——已用 grep 核实这些层当前不含任何 `"wda"` 字面量硬编码,`driver-registry` 既有 Requirement("Adding a driver type requires no changes outside the driver layer")保证新增驱动类型无需改动这些层。
|
||||
- **Non-Goals**(本次明确不做):不新增 `docs/ANDROID_SETUP.md` 或同等深度的 Android SDK/adb 真机安装手册(用户已确认,留到驱动落地、有真机可验证后再开后续变更);不支持 Appium 的 Espresso driver(与 WDA 只做 XCUITest、不支持其他 iOS 后端对称);不做 `APEX_WDA_*` → `DEVICE_RUNTIME_WDA_*` 环境变量重命名(文档中记录的独立遗留事项,与本次无关)。
|
||||
@@ -0,0 +1,16 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Driver type registry lives in the driver layer
|
||||
The system SHALL provide a registry, owned by the `driver` package, that maps a `driver_type` string (e.g. `"wda"`, `"uiautomator2"`) to a builder function producing a `DriverFactory` for that type, so that any caller needing to construct a driver for a device does so without importing a concrete driver class directly.
|
||||
|
||||
#### Scenario: Building a factory for a known driver type
|
||||
- **WHEN** a caller requests a driver factory for `driver_type="wda"` with connection info (e.g. `server_url`, `udid`)
|
||||
- **THEN** the registry returns a `DriverFactory` that, when invoked, constructs a working `WDADriver` configured with that connection info
|
||||
|
||||
#### Scenario: Building a factory for the Android driver type
|
||||
- **WHEN** a caller requests a driver factory for `driver_type="uiautomator2"` with connection info (e.g. `server_url`, `udid`)
|
||||
- **THEN** the registry returns a `DriverFactory` that, when invoked, constructs a working `AndroidDriver` configured with that connection info
|
||||
|
||||
#### Scenario: Building a factory for an unknown driver type
|
||||
- **WHEN** a caller requests a driver factory for a `driver_type` that is not registered
|
||||
- **THEN** the registry raises a clear error naming the unsupported `driver_type`, instead of returning `None` or a factory that fails later at connect time
|
||||
@@ -0,0 +1,36 @@
|
||||
## 1. Verify Appium UiAutomator2 command surface
|
||||
|
||||
- [ ] 1.1 Read the installed `Appium-Python-Client` UiAutomator2 driver source/docs (`appium.options.android.uiautomator2`, `appium.webdriver` extensions) to confirm the exact mobile-command names and parameters for: a tap/click gesture at a coordinate, a coordinate-to-coordinate swipe/drag with duration, and a home-button press. Confirm the exact capability key name for per-session port isolation (UiAutomator2's analog to WDA's `wdaLocalPort`).
|
||||
|
||||
## 2. Driver implementation
|
||||
|
||||
- [ ] 2.1 Add `driver/android_driver.py` with `AndroidDriverConfig` (frozen dataclass): `server_url` (default `http://127.0.0.1:4723`), `platform_name` (default `"Android"`), `automation_name` (default `"UiAutomator2"`), `device_name`, `udid`, `system_port`, `no_reset` (default `True`), `extra_capabilities`.
|
||||
- [ ] 2.2 Implement `AndroidDriver(Driver).connect()`/`disconnect()` using `appium.webdriver` + `UiAutomator2Options`, building capabilities the same way `WDADriver.connect()` does, with the same `_require_client()` guard and `DeviceOfflineError` on connect failure.
|
||||
- [ ] 2.3 Implement `screenshot()`, `tree()`, `input()`, `launch()`, `terminate()`, `lock()`, `unlock()` using the same cross-platform Appium client methods `WDADriver` already uses (`get_screenshot_as_png`, `page_source`, `switch_to.active_element.send_keys`, `activate_app`, `terminate_app`, `lock`, `unlock`).
|
||||
- [ ] 2.4 Implement `tap()`, `swipe()`, `home()` using the mobile-command names/parameters confirmed in task 1.1.
|
||||
- [ ] 2.5 Wrap every method's underlying exception into `DriverError` (`DeviceOfflineError` for connect failure and for calls made before a client exists), matching `WDADriver`'s try/except-per-method pattern exactly.
|
||||
|
||||
## 3. Registry wiring
|
||||
|
||||
- [ ] 3.1 Add `build_android_driver_factory` to `driver/registry.py`, mirroring `build_wda_driver_factory`'s logic for splitting `connection_info` into declared `AndroidDriverConfig` fields vs. `extra_capabilities`.
|
||||
- [ ] 3.2 Register `SUPPORTED_DRIVER_TYPES["uiautomator2"] = build_android_driver_factory`.
|
||||
|
||||
## 4. Unit tests
|
||||
|
||||
- [ ] 4.1 Add mocked unit tests for `AndroidDriver` (mock `appium.webdriver.Remote`, no real device/emulator) covering: connect builds a client with the expected capabilities from a given `AndroidDriverConfig`; connect failure raises `DeviceOfflineError`; calling any operation before `connect()` raises `DeviceOfflineError`; each operation's underlying exception is wrapped into `DriverError`.
|
||||
- [ ] 4.2 Add a unit test for `build_android_driver_factory` covering `connection_info` field extraction and `extra_capabilities` merging (mirror `build_wda_driver_factory`'s existing test coverage if any exists; if none exists today, note that in the test file rather than silently skipping equivalent WDA coverage).
|
||||
|
||||
## 5. Integration test
|
||||
|
||||
- [ ] 5.1 Add `tests/test_android_integration.py` mirroring `tests/test_wda_integration.py`'s structure: `@pytest.mark.integration`, `pytest.skip` when `APEX_ANDROID_SERVER_URL` is unset, optional `APEX_ANDROID_UDID`/`APEX_ANDROID_DEVICE_NAME`, connects and asserts a non-empty `screenshot()` before disconnecting.
|
||||
|
||||
## 6. Spec and docs
|
||||
|
||||
- [ ] 6.1 Confirm `openspec/changes/android-driver/specs/driver-registry/spec.md`'s `driver_type="uiautomator2"` scenario still matches the shipped registry key and behavior exactly; update the delta if anything changed during implementation (e.g. the system-port capability name).
|
||||
- [ ] 6.2 Correct `docs/MACOS_IPHONE_SETUP.md` §1: replace "Android 只是架构上的未来目标,当前 driver/registry.py 没有注册 Android Driver" with an accurate statement that the Android driver is registered, while a full real-device setup guide remains separate follow-up work.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- [ ] 7.1 Run `uv run --all-packages pytest -m "not integration"` and confirm no regressions.
|
||||
- [ ] 7.2 Run the project's lint/format checks against the new files and fix any violations.
|
||||
- [ ] 7.3 Run `openspec validate android-driver --strict` and confirm it passes.
|
||||
Reference in New Issue
Block a user