Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions autowsgr/server/task_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ async def _notify_completion(self, task: TaskInfo) -> None:
success = task.status == TaskStatus.COMPLETED
result = (
task.result_summary
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED)
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.STOPPED)
else None
)
await ws_manager.send_task_completed(
Expand Down Expand Up @@ -322,7 +322,7 @@ def get_status(self) -> dict[str, Any]:

task = self._current_task
result = None
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED):
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.STOPPED):
result = task.result_summary

return {
Expand Down
152 changes: 152 additions & 0 deletions testing/server/test_task_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

from __future__ import annotations

import asyncio
import threading
import time
from typing import Any

import pytest

from autowsgr.server import task_manager as task_manager_module
from autowsgr.server.device_lease import DeviceOperationBusyError, DeviceOperationLease
from autowsgr.server.task_manager import TaskManager, TaskOutcome, TaskStatus

Expand Down Expand Up @@ -227,3 +230,152 @@ def fail_start(_thread: threading.Thread) -> None:

assert manager.current_task is None
assert lease.owner is None


def test_stopped_task_exposes_completed_results_in_status_and_notification(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Cancellation preserves completed rounds for polling and WebSocket clients."""
completed_results = [
{'round': 1, 'success': True},
{'round': 2, 'success': True},
]
completion_calls: list[dict[str, Any]] = []
worker_ready = threading.Event()
release_worker = threading.Event()

async def run_scenario() -> tuple[TaskManager, str]:
notification_sent = asyncio.Event()

async def record_completion(
task_id: str,
success: bool,
result: dict[str, Any] | None = None,
error: str | None = None,
) -> None:
completion_calls.append(
{
'task_id': task_id,
'success': success,
'result': result,
'error': error,
}
)
notification_sent.set()

monkeypatch.setattr(
task_manager_module.ws_manager,
'send_task_completed',
record_completion,
)
manager = TaskManager()
manager.set_loop(asyncio.get_running_loop())

def executor(_task: object) -> TaskOutcome:
worker_ready.set()
release_worker.wait(timeout=1)
return TaskOutcome.from_results(completed_results)

task_id = manager.start_task(
task_type='normal_fight',
total_rounds=3,
executor=executor,
)
assert await asyncio.to_thread(worker_ready.wait, 1)
assert manager.stop_task() is True
assert completion_calls == []

release_worker.set()
assert await asyncio.to_thread(manager.wait_for_completion, 1) is True
await asyncio.wait_for(notification_sent.wait(), timeout=1)
return manager, task_id

manager, task_id = asyncio.run(run_scenario())
expected_result = {
'total_runs': 3,
'success_runs': 2,
'details': completed_results,
}

assert manager.get_status()['result'] == expected_result
assert completion_calls == [
{
'task_id': task_id,
'success': False,
'result': expected_result,
'error': None,
}
]


def test_stopped_task_before_first_round_exposes_empty_summary(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Cancellation before work starts reports an empty result summary, not null."""
completion_calls: list[dict[str, Any]] = []
worker_ready = threading.Event()
release_worker = threading.Event()

async def run_scenario() -> tuple[TaskManager, str]:
notification_sent = asyncio.Event()

async def record_completion(
task_id: str,
success: bool,
result: dict[str, Any] | None = None,
error: str | None = None,
) -> None:
completion_calls.append(
{
'task_id': task_id,
'success': success,
'result': result,
'error': error,
}
)
notification_sent.set()

monkeypatch.setattr(
task_manager_module.ws_manager,
'send_task_completed',
record_completion,
)
manager = TaskManager()
manager.set_loop(asyncio.get_running_loop())

def executor(_task: object) -> TaskOutcome:
worker_ready.set()
release_worker.wait(timeout=1)
return TaskOutcome.from_results([])

task_id = manager.start_task(
task_type='normal_fight',
total_rounds=3,
executor=executor,
)
assert await asyncio.to_thread(worker_ready.wait, 1)
assert manager.stop_task() is True
release_worker.set()
assert await asyncio.to_thread(manager.wait_for_completion, 1) is True
await asyncio.wait_for(notification_sent.wait(), timeout=1)
return manager, task_id

manager, task_id = asyncio.run(run_scenario())
expected_result = {
'total_runs': 3,
'success_runs': 0,
'details': [],
}

assert manager.current_task is not None
assert manager.current_task.status is TaskStatus.STOPPED
assert manager.current_task.error is None
assert manager.get_status()['result'] == expected_result
assert completion_calls == [
{
'task_id': task_id,
'success': False,
'result': expected_result,
'error': None,
}
]
Loading