From 16bcbf5a274aed1e3a96e42a824a9c66a950e409 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 12 Jul 2026 19:57:03 +0800 Subject: [PATCH] feat(cloud-sdk): expose distributed task status --- .../cloud-control-plane-integration/tasks.md | 2 +- packages/cloud-platform/cloud/sdk/api.py | 3 ++ packages/cloud-platform/cloud/sdk/models.py | 4 ++ tests/test_cloud_sdk_api.py | 43 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/openspec/changes/cloud-control-plane-integration/tasks.md b/openspec/changes/cloud-control-plane-integration/tasks.md index 584b158..3666662 100644 --- a/openspec/changes/cloud-control-plane-integration/tasks.md +++ b/openspec/changes/cloud-control-plane-integration/tasks.md @@ -63,7 +63,7 @@ ## 8. Public SDK And Operational Delivery -- [ ] 8.1 Extend public task status models/routes with attempt count, lease expiry metadata, and terminal failure details without exposing lease credentials. +- [x] 8.1 Extend public task status models/routes with attempt count, lease expiry metadata, and terminal failure details without exposing lease credentials. - [ ] 8.2 Add bearer authentication and typed authorization errors to `CloudClient` while preserving injectable HTTP clients for tests. - [ ] 8.3 Add container definitions and example environment configuration for the cloud API, PostgreSQL, and Host Agent without committing secrets. - [ ] 8.4 Document local SQLite startup, deployed PostgreSQL migration/startup, credential/scopes setup, Runtime AI Planner configuration, and shutdown/rollback procedures. diff --git a/packages/cloud-platform/cloud/sdk/api.py b/packages/cloud-platform/cloud/sdk/api.py index 6cd43a3..6fc2011 100644 --- a/packages/cloud-platform/cloud/sdk/api.py +++ b/packages/cloud-platform/cloud/sdk/api.py @@ -107,6 +107,9 @@ def create_cloud_router( workflow_definition_id=task.workflow_definition_id, assigned_device_id=task.assigned_device_id, assigned_host_id=task.assigned_host_id, + attempt_count=task.attempt_count, + lease_expires_at=task.lease_expires_at, + failure_reason=task.failure_reason, ) @router.get("/devices", response_model=list[DeviceResponse]) diff --git a/packages/cloud-platform/cloud/sdk/models.py b/packages/cloud-platform/cloud/sdk/models.py index 4238a2e..b8b6c3f 100644 --- a/packages/cloud-platform/cloud/sdk/models.py +++ b/packages/cloud-platform/cloud/sdk/models.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import datetime from typing import Literal from pydantic import BaseModel, Field @@ -29,6 +30,9 @@ class TaskStatusResponse(BaseModel): workflow_definition_id: str | None = None assigned_device_id: str | None = None assigned_host_id: str | None = None + attempt_count: int = 0 + lease_expires_at: datetime | None = None + failure_reason: str | None = None class DeviceResponse(BaseModel): diff --git a/tests/test_cloud_sdk_api.py b/tests/test_cloud_sdk_api.py index d35b46d..d2a0ac5 100644 --- a/tests/test_cloud_sdk_api.py +++ b/tests/test_cloud_sdk_api.py @@ -2,6 +2,8 @@ from __future__ import annotations +from datetime import UTC, datetime + import pytest from cloud.config import CloudConfig @@ -92,6 +94,47 @@ def test_unknown_task_id_returns_404(tmp_path) -> None: assert resp.status_code == 404, resp.text +def test_task_status_exposes_distributed_metadata_without_lease_secret( + tmp_path, +) -> None: + app, pool, scheduler, _ = _build_app(tmp_path) + pool.sync_host_devices( + "host-a", + [Device(id="device-a", driver_type="wda", status="idle")], # type: ignore[arg-type] + ) + task_id = scheduler.submit(goal="remote task") + scheduler.assign() + + active = _client_for(app).get(f"/v1/tasks/{task_id}").json() + + assert active["status"] == "assigned" + assert active["assigned_host_id"] == "host-a" + assert active["assigned_device_id"] == "device-a" + assert active["attempt_count"] == 1 + assert active["lease_expires_at"] is not None + assert active["failure_reason"] is None + assert "lease_id" not in active + + task = scheduler.store.get_task(task_id) + scheduler.store.record_task_result( + task_id=task_id, + attempt=task.attempt_count, + lease_id=task.lease_id or "", + host_id=task.assigned_host_id or "", + status="failed", + failure_reason="planner unavailable", + terminal_result={"runtime_status": "failed"}, + completed_at=datetime.now(UTC), + ) + + failed = _client_for(app).get(f"/v1/tasks/{task_id}").json() + + assert failed["status"] == "failed" + assert failed["attempt_count"] == 1 + assert failed["failure_reason"] == "planner unavailable" + assert "lease_id" not in failed + + def test_device_and_host_listing_reflect_pool_state(tmp_path) -> None: app, pool, _, _ = _build_app(tmp_path) pool.sync_host_devices(