feat(cloud-sdk): expose distributed task status

This commit is contained in:
2026-07-12 19:57:03 +08:00
parent 77fadbd9e6
commit 16bcbf5a27
4 changed files with 51 additions and 1 deletions
@@ -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.
+3
View File
@@ -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])
@@ -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):
+43
View File
@@ -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(