refactor: simplify task and history services
Remove retired query paths and repository compatibility branches, and share task test fixtures. Cancel pending questions and resolve their notifications when recovering expired runs.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID, uuid4
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -10,7 +11,7 @@ from app.domain.schemas.operational_task import AgentRunCommand, AgentRunFailure
|
||||
from app.domain.schemas.task_question import TaskQuestionCreate, TaskQuestionReply
|
||||
from app.services.operational_tasks import OperationalTaskService
|
||||
from app.services.task_questions import create_question, answer_question
|
||||
from tests.unit.test_operational_tasks import RunTaskRepository, _context, _task_document, _active_run
|
||||
from tests.support.operational_task_repository import TaskRepository, _context, _task_document, _active_run
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,28 +19,11 @@ def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
class QuestionRepository(RunTaskRepository):
|
||||
def __init__(self, task, run):
|
||||
super().__init__(task, run)
|
||||
self.questions = {}
|
||||
|
||||
async def list_questions(self, task_id):
|
||||
return [deepcopy(q) for q in self.questions.values() if q["task_id"] == task_id]
|
||||
|
||||
async def get_question(self, task_id, question_id):
|
||||
q = self.questions.get(question_id)
|
||||
return deepcopy(q) if q and q["task_id"] == task_id else None
|
||||
|
||||
async def save_question(self, task_id, document):
|
||||
assert document["task_id"] == task_id
|
||||
self.questions[document["question_request_id"]] = deepcopy(document)
|
||||
|
||||
|
||||
def setup():
|
||||
now = datetime(2026, 10, 30, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repo = QuestionRepository(task, _active_run(task, now))
|
||||
repo = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repo, context, {OPERATIONAL_TASK_EXECUTE, OPERATIONAL_TASK_WRITE}, clock=lambda: now)
|
||||
command = TaskQuestionCreate(request_id=uuid4(), source_session_id="session-1", run_id=repo.run["run_id"],
|
||||
questions=[dict(question_id="flow", title="漏水量依据", body="是否有计量记录?", options=[{"id": "unknown", "label": "目前未知"}],
|
||||
@@ -162,6 +146,34 @@ async def test_real_execution_failure_cancels_question_instead_of_disguising_fai
|
||||
assert repo.notifications[0]["lifecycle_status"] == "resolved"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("timeout_count", [1, 3])
|
||||
async def test_lease_timeout_cancels_old_question_and_unblocks_the_next_generation(timeout_count):
|
||||
service, repo, task_id, command = setup()
|
||||
question = await create_question(service, task_id, command)
|
||||
service.clock = lambda: question.created_at + timedelta(minutes=2)
|
||||
repo.claim_expired_runs = AsyncMock(return_value=[deepcopy(repo.run)])
|
||||
repo.consecutive_timed_out_runs = AsyncMock(return_value=timeout_count)
|
||||
|
||||
await service.recover_expired_leases(1)
|
||||
|
||||
assert repo.questions[str(question.question_request_id)]["status"] == "cancelled"
|
||||
assert repo.notifications[0]["lifecycle_status"] == "resolved"
|
||||
with pytest.raises(DomainError):
|
||||
await answer_question(service, task_id, question.question_request_id, reply())
|
||||
if timeout_count == 3:
|
||||
assert repo.task["lifecycle_status"] == "failed"
|
||||
return
|
||||
|
||||
await service.claim_run(task_id, UUID(repo.run["run_id"]), AgentRunCommand(
|
||||
request_id=UUID(repo.run["execution_request_id"]), expected_revision=1,
|
||||
source_session_id="session-2"))
|
||||
next_question = await create_question(service, task_id, command.model_copy(update={
|
||||
"request_id": uuid4(), "run_id": UUID(repo.run["run_id"]), "source_session_id": "session-2"}))
|
||||
assert next_question.status == "pending"
|
||||
assert next_question.execution_generation == 2
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_multiple_selection_and_optional_question_are_preserved():
|
||||
service, repo, task_id, command = setup()
|
||||
|
||||
Reference in New Issue
Block a user