Merge branch 'worktree-task-cancellation': task cancellation feature
Tests / Test passed: 926

# Conflicts:
#	packages/cloud-platform/cloud/schema.py
This commit is contained in:
2026-07-15 19:39:28 +08:00
49 changed files with 2374 additions and 63 deletions
+189 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
import pytest
@@ -380,6 +380,7 @@ def test_submit_rejects_incomplete_or_foreign_target(tmp_path) -> None:
("get", "/v1/tasks", None, "tasks:read"),
("get", "/v1/tasks/missing/attempts", None, "tasks:read"),
("get", "/v1/tasks/missing/planner-decisions?attempt=0", None, "tasks:read"),
("post", "/v1/tasks/missing/cancel", None, "tasks:submit"),
("get", "/v1/devices", None, "pool:read"),
("get", "/v1/hosts", None, "pool:read"),
("get", "/v1/plugins", None, "plugins:read"),
@@ -534,6 +535,193 @@ def test_list_task_attempts_returns_404_for_unknown_task(tmp_path) -> None:
assert "does-not-exist" in resp.json()["detail"]
def test_cancel_queued_task_transitions_immediately(tmp_path) -> None:
app, _, scheduler, _ = _build_app(tmp_path)
task_id = scheduler.submit(goal="cancel me")
resp = _client_for(app).post(f"/v1/tasks/{task_id}/cancel")
assert resp.status_code == 200, resp.text
assert resp.json() == {"task_id": task_id, "status": "cancelled"}
assert scheduler.store.get_task(task_id).status == "cancelled"
def test_cancel_assigned_task_records_pending_request(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="cancel me mid-flight")
scheduler.assign()
resp = _client_for(app).post(f"/v1/tasks/{task_id}/cancel")
assert resp.status_code == 202, resp.text
assert resp.json() == {"task_id": task_id, "status": "assigned"}
task = scheduler.store.get_task(task_id)
assert task.status == "assigned"
assert task.cancel_requested_at is not None
def test_cancel_repeat_call_on_pending_request_is_idempotent(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="cancel me twice")
scheduler.assign()
client = _client_for(app)
first = client.post(f"/v1/tasks/{task_id}/cancel")
second = client.post(f"/v1/tasks/{task_id}/cancel")
assert first.status_code == 202, first.text
assert second.status_code == 200, second.text
assert second.json() == {"task_id": task_id, "status": "assigned"}
def test_cancel_repeat_call_on_already_cancelled_task_is_idempotent(
tmp_path,
) -> None:
app, _, scheduler, _ = _build_app(tmp_path)
task_id = scheduler.submit(goal="cancel me twice")
client = _client_for(app)
first = client.post(f"/v1/tasks/{task_id}/cancel")
second = client.post(f"/v1/tasks/{task_id}/cancel")
assert first.status_code == 200, first.text
assert second.status_code == 200, second.text
assert second.json() == {"task_id": task_id, "status": "cancelled"}
def test_cancel_unknown_task_returns_404(tmp_path) -> None:
app, _, _, _ = _build_app(tmp_path)
resp = _client_for(app).post("/v1/tasks/does-not-exist/cancel")
assert resp.status_code == 404, resp.text
assert "does-not-exist" in resp.json()["detail"]
def test_cancel_terminal_task_returns_409(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="finish me")
scheduler.assign()
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="done",
failure_reason=None,
terminal_result={"runtime_status": "completed"},
completed_at=datetime.now(UTC),
)
resp = _client_for(app).post(f"/v1/tasks/{task_id}/cancel")
assert resp.status_code == 409, resp.text
assert "terminal" in resp.json()["detail"]
def test_cancel_scope_rejected_before_reaching_scheduler(tmp_path, monkeypatch) -> None:
provider = ConfiguredBearerAuthProvider(
[
BearerCredential(
principal_id="reader",
token="reader-token",
scopes=frozenset({"tasks:read"}),
)
]
)
app, _, scheduler, _ = _build_app(tmp_path, auth_provider=provider)
called = False
def fail_if_called(*args, **kwargs):
nonlocal called
called = True
raise AssertionError("cancellation must not run before authorization")
monkeypatch.setattr(scheduler.store, "request_task_cancellation", fail_if_called)
resp = _client_for(app).post(
"/v1/tasks/does-not-exist/cancel",
headers={"Authorization": "Bearer reader-token"},
)
assert resp.status_code == 403
assert called is False
def test_cancellation_full_path_queued_immediate_and_dispatched_collaborative(
tmp_path,
) -> None:
"""End-to-end exercise of the cancellation path (task-cancellation 9.2):
a queued task is cancelled immediately; a dispatched task's cancellation
is only recorded until the Host Agent's next lease renewal surfaces it
and reports back a cancelled terminal result, after which the task is
visible as cancelled via the public API's get and list endpoints."""
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]
)
client = _client_for(app)
queued_task_id = scheduler.submit(goal="cancel me while queued")
queued_cancel = client.post(f"/v1/tasks/{queued_task_id}/cancel")
assert queued_cancel.status_code == 200, queued_cancel.text
assert queued_cancel.json() == {"task_id": queued_task_id, "status": "cancelled"}
dispatched_task_id = scheduler.submit(goal="cancel me mid-execution")
scheduler.assign()
dispatched_cancel = client.post(f"/v1/tasks/{dispatched_task_id}/cancel")
assert dispatched_cancel.status_code == 202, dispatched_cancel.text
assert dispatched_cancel.json() == {
"task_id": dispatched_task_id,
"status": "assigned",
}
task = scheduler.store.get_task(dispatched_task_id)
renewed_at = datetime.now(UTC)
renewal = scheduler.store.renew_lease(
task_id=dispatched_task_id,
attempt=task.attempt_count,
lease_id=task.lease_id or "",
host_id=task.assigned_host_id or "",
lease_expires_at=renewed_at + timedelta(seconds=30),
now=renewed_at,
)
assert renewal.status == "renewed", renewal
assert renewal.cancel_requested is True
scheduler.store.record_task_result(
task_id=dispatched_task_id,
attempt=task.attempt_count,
lease_id=task.lease_id or "",
host_id=task.assigned_host_id or "",
status="cancelled",
failure_reason="cancellation requested by control plane",
terminal_result=None,
completed_at=datetime.now(UTC),
)
status_resp = client.get(f"/v1/tasks/{dispatched_task_id}")
assert status_resp.status_code == 200, status_resp.text
assert status_resp.json()["status"] == "cancelled"
list_resp = client.get("/v1/tasks", params={"status": "cancelled"})
assert list_resp.status_code == 200, list_resp.text
cancelled_ids = {t["id"] for t in list_resp.json()["items"]}
assert {queued_task_id, dispatched_task_id} <= cancelled_ids
def test_plugin_admin_scope_is_checked_before_registration(
tmp_path,
monkeypatch,