Add end-to-end cancellation path test and verify no regressions (task-cancellation 9.1-9.2)

- test_cancellation_full_path_queued_immediate_and_dispatched_collaborative
  exercises the full public-API cancellation path: immediate cancel of a
  queued task, collaborative cancel of a dispatched task surfaced through
  lease renewal and a cancelled terminal report, and visibility of the
  cancelled status via both the get and list endpoints.
- Full backend suite (869 passed, 50 skipped) and cloud-console frontend
  suite (27 passed) + typecheck show no regressions; the only failures are
  4 pre-existing live-LLM integration tests unrelated to this change.
This commit is contained in:
2026-07-15 19:23:35 +08:00
parent 1dd24825ca
commit fd6365cf6e
2 changed files with 66 additions and 3 deletions
+2 -2
View File
@@ -63,5 +63,5 @@
## 9. End-to-end verification
- [ ] 9.1 Run the full test suite (`uv run pytest` at repo root, plus `cloud-console` frontend tests) and confirm no regressions in existing task-scheduler, host-agent-protocol, platform-sdk, or workflow-orchestration tests.
- [ ] 9.2 Manually or via an integration test, exercise the full path: submit a task, cancel a `queued` task (immediate), submit and dispatch another task, cancel it mid-execution, and confirm it reaches `cancelled` within one lease-renewal cycle with `cancelled` visible in both the public API and the Cloud Console.
- [x] 9.1 Run the full test suite (`uv run pytest` at repo root, plus `cloud-console` frontend tests) and confirm no regressions in existing task-scheduler, host-agent-protocol, platform-sdk, or workflow-orchestration tests. (869 passed, 50 skipped, 4 pre-existing failures unrelated to this change — `test_verifier_against_real_llm`, `test_reflector_against_real_llm`, `test_real_anthropic_ai_planner_selects_a_tool`, `test_real_anthropic_semantic_enrichment_returns_schema_valid_scene` all require live Anthropic API network access and fail the same way on `master`. `cloud-console`: 27 tests passed, `vue-tsc --noEmit` typecheck clean.)
- [x] 9.2 Manually or via an integration test, exercise the full path: submit a task, cancel a `queued` task (immediate), submit and dispatch another task, cancel it mid-execution, and confirm it reaches `cancelled` within one lease-renewal cycle with `cancelled` visible in both the public API and the Cloud Console. (Added `test_cancellation_full_path_queued_immediate_and_dispatched_collaborative` in `tests/test_cloud_sdk_api.py`: submits and immediately cancels a queued task via `POST /v1/tasks/{id}/cancel` (200, `cancelled`); submits, dispatches, and cancels a second task mid-execution (202, pending); drives one lease renewal confirming `cancel_requested=True` is surfaced; reports a `cancelled` terminal result as the Host Agent would; and confirms the task shows `status="cancelled"` via both `GET /v1/tasks/{id}` and `GET /v1/tasks?status=cancelled`. The Cloud Console reads task status through this same public API and its `cancelled` rendering is covered by the Task 6.4 `taskCancellation.ts` unit tests, so this repository-to-API round trip is the full path exercised at the automated-test layer; no manual browser session was run.)
+64 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
import pytest
@@ -620,6 +620,69 @@ def test_cancel_scope_rejected_before_reaching_scheduler(tmp_path, monkeypatch)
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,