From 90961bd0a4d61dafe69433982f7357f054d47281 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 13 Jul 2026 07:56:41 +0800 Subject: [PATCH] fix(host-agent): load configured devices --- .env.example | 1 + apps/device-host-agent/host_agent/app.py | 24 ++++++++++++++++++++- apps/device-host-agent/tests/test_app.py | 27 ++++++++++++++++++++++++ compose.yaml | 2 ++ tests/test_deployment_config.py | 3 +++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 9b9346d..213db69 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ CLOUD_MAX_TASK_ATTEMPTS=3 HOST_AGENT_HOST_ID=host-local HOST_AGENT_TOKEN=change-me-host-token +HOST_AGENT_TASKS_PATH=./tasks HOST_AGENT_HEARTBEAT_INTERVAL_SECONDS=30 HOST_AGENT_POLL_TIMEOUT_SECONDS=20 HOST_AGENT_RETRY_BACKOFF_SECONDS=1 diff --git a/apps/device-host-agent/host_agent/app.py b/apps/device-host-agent/host_agent/app.py index cc865bc..5acff64 100644 --- a/apps/device-host-agent/host_agent/app.py +++ b/apps/device-host-agent/host_agent/app.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from cloud.internal_api.models import AssignmentModel from device.manager import DeviceManager +from driver.registry import build_driver_factory from host_agent.assignment import AssignmentExecutor from host_agent.client import HostAgentClient from host_agent.config import HostAgentConfig, load_host_agent_config @@ -13,6 +14,7 @@ from host_agent.execution import create_execution_factories from host_agent.heartbeat import HeartbeatSynchronizer from host_agent.lease import ActiveAssignmentRunner from host_agent.processor import AssignmentProcessingResult, AssignmentProcessor +from storage.device_config import DeviceConfigStore @dataclass @@ -94,9 +96,10 @@ def create_application( *, config: HostAgentConfig | None = None, manager: DeviceManager | None = None, + device_config_store: DeviceConfigStore | None = None, ) -> HostAgentApplication: resolved_config = config or load_host_agent_config() - resolved_manager = manager or DeviceManager() + resolved_manager = manager or _configured_device_manager(device_config_store) client = HostAgentClient(resolved_config) heartbeat = HeartbeatSynchronizer(resolved_manager, client, resolved_config) executor = AssignmentExecutor(create_execution_factories(resolved_manager)) @@ -106,3 +109,22 @@ def create_application( heartbeat=heartbeat, processor=AssignmentProcessor(client, active_runner), ) + + +def _configured_device_manager( + config_store: DeviceConfigStore | None, +) -> DeviceManager: + manager = DeviceManager() + store = config_store or DeviceConfigStore() + for device in store.list(): + manager.register_device( + device["device_id"], + build_driver_factory( + device["driver_type"], + device["connection_info"], + ), + name=device["name"], + driver_type=device["driver_type"], + connection_info=device["connection_info"], + ) + return manager diff --git a/apps/device-host-agent/tests/test_app.py b/apps/device-host-agent/tests/test_app.py index decb740..6be9c5d 100644 --- a/apps/device-host-agent/tests/test_app.py +++ b/apps/device-host-agent/tests/test_app.py @@ -8,6 +8,7 @@ from cloud.internal_api.models import AssignmentModel from device.manager import DeviceManager from host_agent.app import HostAgentApplication, create_application from host_agent.config import HostAgentConfig +from storage.device_config import DeviceConfigStore def _config() -> HostAgentConfig: @@ -38,6 +39,32 @@ def test_create_application_composes_host_agent_services(tmp_path, monkeypatch) asyncio.run(application.client.aclose()) +def test_create_application_loads_persisted_device_configuration( + tmp_path, + monkeypatch, +) -> None: + monkeypatch.chdir(tmp_path) + store = DeviceConfigStore(tmp_path / "devices.sqlite3") + store.add( + device_id="device-a", + name="Lab iPhone", + driver_type="wda", + connection_info={"url": "http://wda.local"}, + ) + + application = create_application( + config=_config(), + device_config_store=store, + ) + + devices = application.heartbeat.manager.list_devices() + assert [(device.id, device.name, device.driver_type) for device in devices] == [ + ("device-a", "Lab iPhone", "wda") + ] + assert devices[0].connection_info == {"url": "http://wda.local"} + asyncio.run(application.client.aclose()) + + def test_shutdown_cancels_long_poll_and_sends_final_heartbeat() -> None: async def scenario() -> None: claim_started = asyncio.Event() diff --git a/compose.yaml b/compose.yaml index a22df38..b5ea86a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -63,6 +63,8 @@ services: HOST_AGENT_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_RETRY_BACKOFF_SECONDS:-1} HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS:-30} HOST_AGENT_MAX_RETRY_ATTEMPTS: ${HOST_AGENT_MAX_RETRY_ATTEMPTS:-5} + volumes: + - ${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks depends_on: cloud-api: condition: service_healthy diff --git a/tests/test_deployment_config.py b/tests/test_deployment_config.py index 043f201..697b893 100644 --- a/tests/test_deployment_config.py +++ b/tests/test_deployment_config.py @@ -22,6 +22,9 @@ def test_compose_defines_database_control_plane_and_outbound_host_agent() -> Non "service_healthy" ) assert "ports" not in services["host-agent"] + assert services["host-agent"]["volumes"] == [ + "${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks" + ] assert services["host-agent"]["environment"][ "HOST_AGENT_CONTROL_PLANE_URL" ] == "http://cloud-api:8001"