Files
next-tjwater-server/tests/unit/test_operational_tasks.py
T

846 lines
31 KiB
Python

from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from app.auth.permissions import (
OPERATIONAL_ACTION_WORK_ORDER_APPROVE,
OPERATIONAL_ACTION_WORK_ORDER_EXECUTE,
OPERATIONAL_TASK_VIEW,
)
from app.auth.project_dependencies import ProjectContext
from app.domain.domain_errors import DomainError
from app.domain.schemas.operational_task import (
ActionDecisionCommand,
ActionExecuteCommand,
ActionProposalCommand,
AgentArtifactsCommand,
AgentRunCommand,
ExternalFeedbackCommand,
HumanApprovalAction,
NotificationAcknowledgeCommand,
OperationalTaskNotification,
OperationalTaskRun,
OperationalTaskSummary,
)
from app.domain.schemas.operational_data import TaskDataSnapshot
from app.infra.db.postgresql.operational_tasks import OperationalTaskRepository
from app.services.operational_tasks import OperationalTaskService
@pytest.fixture
def anyio_backend():
return "asyncio"
class NotificationRepository:
def __init__(self, notification):
self.notification = notification
self.reads = {}
self.requests = {}
@asynccontextmanager
async def transaction(self):
yield self
async def lock(self, _key):
return None
async def get_request(self, scope, request_id):
return self.requests.get((scope, request_id))
async def save_request(self, scope, request_id, request_digest, response):
self.requests[(scope, request_id)] = {
"request_digest": request_digest,
"response": response,
}
async def get_notification(self, notification_id, user_id):
if notification_id != str(self.notification.notification_id):
return None
read = self.reads.get(user_id)
return self.notification.model_copy(update={
"status": "read" if read else "unread",
"version": 2 if read else 1,
"read_at": read,
"read_by": f"principal:{user_id}" if read else None,
}).model_dump(mode="json")
async def mark_notification_read(self, notification_id, user_id, version, read_at):
assert version == 2
self.reads[user_id] = read_at
def _context(user_id=None, *, system_role="user"):
return ProjectContext(
project_id=uuid4(), project_code="test", user_id=user_id or uuid4(),
project_role="member", system_role=system_role,
)
@pytest.mark.anyio
async def test_notification_read_state_is_isolated_per_user():
now = datetime(2026, 9, 23, tzinfo=timezone.utc)
notification = OperationalTaskNotification(
notification_id=uuid4(), task_id=uuid4(), kind="task_created",
title="任务", description="已创建", status="unread", version=1,
created_at=now,
)
repository = NotificationRepository(notification)
first = _context()
second = _context()
first_service = OperationalTaskService(
repository, first, {OPERATIONAL_TASK_VIEW}, clock=lambda: now
)
second_service = OperationalTaskService(
repository, second, {OPERATIONAL_TASK_VIEW}, clock=lambda: now
)
request_id = uuid4()
read = await first_service.acknowledge_notification(
notification.notification_id,
NotificationAcknowledgeCommand(request_id=request_id, expected_version=1),
)
assert read.status == "read"
assert (await repository.get_notification(
str(notification.notification_id), str(second.user_id)
))["status"] == "unread"
second_read = await second_service.acknowledge_notification(
notification.notification_id,
NotificationAcknowledgeCommand(request_id=request_id, expected_version=1),
)
assert second_read.status == "read"
assert second_read.read_by == f"principal:{second.user_id}"
def test_running_plan_state_wins_over_future_human_waiting_node():
nodes = [
type("Node", (), {"status": "running", "owner": "agent"})(),
type("Node", (), {"status": "waiting", "owner": "human"})(),
]
assert OperationalTaskService._status_from_plan(nodes, []) == "running"
def test_pending_approval_prevents_plan_from_marking_task_completed():
action = HumanApprovalAction(
action_id=uuid4(), task_id=uuid4(), version=1,
kind="create_work_order", reason="现场采样",
payload={"title": "采样", "description": "核验", "work_order_type": "sampling"},
evidence_refs=[], required_approval_permission="work-order.approve",
required_execution_permission="work-order.execute",
approval_status="pending", execution_status="not_submitted",
proposed_by="principal:test", proposed_at=datetime.now(timezone.utc),
)
nodes = [type("Node", (), {"status": "completed", "owner": "agent"})()]
assert OperationalTaskService._status_from_plan(nodes, [action]) == "waiting_human"
def test_explicit_agent_run_failure_marks_task_failed_even_with_waiting_work():
nodes = [
type("Node", (), {"id": "FIELD_SAMPLE", "status": "waiting", "owner": "human"})(),
type("Node", (), {"id": "AGENT_RUN_FAILURE", "status": "failed", "owner": "agent"})(),
]
assert OperationalTaskService._status_from_plan(nodes, []) == "failed"
@pytest.mark.anyio
async def test_targeted_notification_cannot_be_acknowledged_without_permission():
now = datetime(2026, 9, 23, tzinfo=timezone.utc)
notification = OperationalTaskNotification(
notification_id=uuid4(), task_id=uuid4(), kind="action_pending",
title="待确认", description="命令", required_permission="command.approve",
status="unread", version=1, created_at=now,
)
service = OperationalTaskService(
NotificationRepository(notification), _context(), {OPERATIONAL_TASK_VIEW},
clock=lambda: now,
)
with pytest.raises(DomainError) as exc:
await service.acknowledge_notification(
notification.notification_id,
NotificationAcknowledgeCommand(request_id=uuid4(), expected_version=1),
)
assert exc.value.code == "PERMISSION_DENIED"
def _task_document(context, now, *, lease=None):
task_id = uuid4()
return OperationalTaskSummary(
project_id=context.project_id,
task_id=task_id,
task_type="event_investigation",
workflow="water_quality_complaint",
status="running",
priority="normal",
revision=2,
execution_generation=1,
execution_request_id=uuid4(),
title="调查",
summary="验证状态机",
source={"kind": "operator", "event_kind": "water_quality_complaint"},
trigger={"kind": "manual", "occurred_at": now, "detail": "test"},
scheduled_for=None,
agent_session_id="session-1",
agent_lease_expires_at=lease,
current_plan_revision=None,
current_report_revision=None,
created_at=now,
updated_at=now,
).model_dump(mode="json")
class TaskRepository:
def __init__(self, task, *, action=None):
self.task = task
self.action = action
self.requests = {}
self.evidence = {}
self.feedback = {}
self.plans = []
self.reports = []
self.notifications = []
self.locks = []
@asynccontextmanager
async def transaction(self):
yield self
async def lock(self, key):
self.locks.append(key)
async def get_request(self, scope, request_id):
return self.requests.get((scope, request_id))
async def save_request(self, scope, request_id, request_digest, response):
self.requests[(scope, request_id)] = {
"request_digest": request_digest,
"response": response,
}
async def get_task(self, task_id):
return self.task if task_id == self.task["task_id"] else None
async def update_task(self, task):
self.task = task
async def get_latest_plan(self, _task_id):
return self.plans[-1] if self.plans else None
async def get_latest_report(self, _task_id):
return self.reports[-1] if self.reports else None
async def insert_plan(self, _task_id, snapshot):
self.plans.append(snapshot)
async def insert_report(self, _task_id, snapshot):
self.reports.append(snapshot)
async def list_evidence(self, _task_id):
return list(self.evidence.values())
async def get_evidence(self, _task_id, evidence_id):
return self.evidence.get(evidence_id)
async def insert_evidence(self, _task_id, evidence):
self.evidence[evidence["evidence_id"]] = evidence
async def list_actions(self, _task_id):
return [self.action] if self.action else []
async def get_action(self, _task_id, action_id):
if self.action and self.action["action_id"] == action_id:
return self.action
return None
async def update_action(self, _task_id, action):
self.action = action
async def insert_action(self, _task_id, action):
self.action = action
async def insert_notification(self, notification):
self.notifications.append(notification)
async def get_feedback(self, task_id, source_event_id):
return self.feedback.get((task_id, source_event_id))
async def upsert_feedback(self, task_id, feedback, accepted_at):
self.feedback[(task_id, feedback["source_event_id"])] = {
"source_revision": feedback["source_revision"],
"document": feedback,
"accepted_at": accepted_at,
}
class RunTaskRepository(TaskRepository):
def __init__(self, task, run):
super().__init__(task)
self.run = run
self.events = []
async def get_active_run(self, task_id):
if task_id == self.task["task_id"] and self.run["status"] in {"queued", "claimed", "running"}:
return self.run
return None
async def get_run(self, run_id):
return self.run if run_id == self.run["run_id"] else None
async def update_run(self, run, expected_revision):
assert run["revision"] == expected_revision + 1
self.run = run
async def insert_run(self, run):
self.run = run
async def append_event(self, **event):
self.events.append(event)
return len(self.events)
def _active_run(task, now):
return OperationalTaskRun(
run_id=uuid4(), task_id=task["task_id"], generation=task["execution_generation"],
execution_request_id=task["execution_request_id"], status="running", revision=2,
agent_session_id="session-1", lease_expires_at=now + timedelta(minutes=1),
heartbeat_at=now, started_at=now, dispatch_attempts=0,
next_dispatch_at=now, created_at=now, updated_at=now,
).model_dump(mode="json")
@pytest.mark.anyio
async def test_run_heartbeat_does_not_change_task_business_revision():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
updated = await service.heartbeat_run(
task["task_id"], repository.run["run_id"],
AgentRunCommand(
request_id=uuid4(), expected_revision=2, source_session_id="session-1"
),
)
assert updated.revision == 3
assert repository.task["revision"] == 2
assert repository.events == []
@pytest.mark.anyio
async def test_complete_without_same_generation_artifacts_fails_run_explicitly():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
completed = await service.complete_run(
task["task_id"], repository.run["run_id"],
AgentRunCommand(
request_id=uuid4(), expected_revision=2, source_session_id="session-1"
),
)
assert repository.run["status"] == "failed"
assert repository.run["failure_code"] == "ARTIFACT_INCOMPLETE"
assert completed.lifecycle_status == "failed"
assert completed.active_run is None
assert [item["kind"] for item in repository.notifications] == ["task_failed"]
@pytest.mark.anyio
async def test_external_feedback_coalesces_into_an_already_queued_run():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=None)
task.update(
status="queued", lifecycle_status="open", execution_generation=2,
execution_request_id=str(uuid4()), agent_session_id=None,
)
run = _active_run(task, now)
run.update(
generation=2, execution_request_id=task["execution_request_id"],
status="queued", revision=1, agent_session_id=None,
lease_expires_at=None, heartbeat_at=None, started_at=None,
)
repository = RunTaskRepository(task, run)
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
result = await service.external_feedback(
task["task_id"],
ExternalFeedbackCommand(
request_id=uuid4(), source_event_id="sample-result", source_revision=1,
status="completed", occurred_at=now, detail="新的现场结果",
),
)
assert result.task.execution_generation == 2
assert str(result.task.execution_request_id) == task["execution_request_id"]
assert result.task.active_run is not None
assert str(result.task.active_run.run_id) == run["run_id"]
def _waiting_plan(action_id=None):
node = {
"id": "field-work",
"title": "现场处置",
"owner": "human",
"status": "waiting",
"detail": "等待审批与执行",
"evidence_refs": [],
"action_refs": [str(action_id)] if action_id else [],
}
return {
"schema": "execution-plan@1",
"title": "调查计划",
"reason": "等待现场处置",
"nodes": [node],
"edges": [],
}
def _current_report():
return {
"title": "本代调查报告",
"current_conclusion": "等待现场处置",
"conclusion_boundary": "尚无现场回执",
"evidence_refs": [],
"recommendations": ["完成现场处置"],
"pending_items": ["现场处置"],
}
@pytest.mark.anyio
async def test_plan_only_keeps_lease_until_same_generation_report_closes_run():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
planned = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", plan=_waiting_plan(),
))
assert planned.task.status == "running"
assert planned.task.agent_lease_expires_at == now + timedelta(minutes=1)
assert planned.task.latest_plan.execution_generation == 1
reported = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", report=_current_report(),
))
assert reported.task.status == "running"
assert reported.task.latest_report.execution_generation == 1
completed = await service.complete_run(
task["task_id"], repository.run["run_id"],
AgentRunCommand(
request_id=uuid4(), expected_revision=repository.run["revision"],
source_session_id="session-1",
),
)
assert completed.status == "waiting_human"
assert completed.agent_lease_expires_at is None
assert [item["kind"] for item in repository.notifications] == [
"human_input_required"
]
@pytest.mark.anyio
async def test_report_does_not_close_from_legacy_or_prior_generation_plan():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
task["execution_generation"] = 2
repository = TaskRepository(task)
repository.plans.append({
"revision": 1,
"execution_generation": None,
"source_session_id": "session-1",
"published_at": now.isoformat(),
"published_by": "principal:test",
"plan": _waiting_plan(),
})
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
result = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", report=_current_report(),
))
assert result.task.status == "running"
assert result.task.latest_report.execution_generation == 2
@pytest.mark.anyio
async def test_action_proposal_keeps_running_lease_until_plan_and_report_close_run():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", plan=_waiting_plan(),
))
revision_before_proposal = repository.run["revision"]
lease_before_proposal = repository.run["lease_expires_at"]
action = await service.propose_action(task["task_id"], ActionProposalCommand(
request_id=uuid4(), kind="create_work_order", reason="需要现场核验",
node_id="field-work", evidence_refs=[],
payload={"title": "现场核验", "description": "等待人工执行", "work_order_type": "sampling"},
))
assert repository.task["status"] == "running"
assert repository.run["revision"] == revision_before_proposal
assert repository.run["lease_expires_at"] == lease_before_proposal
heartbeat = await service.heartbeat_run(
task["task_id"], repository.run["run_id"], AgentRunCommand(
request_id=uuid4(), expected_revision=revision_before_proposal,
source_session_id="session-1",
),
)
assert heartbeat.status == "running"
published = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1",
run_id=repository.run["run_id"],
plan=_waiting_plan(action.action_id), report=_current_report(),
))
closed = await service.complete_run(
task["task_id"], repository.run["run_id"],
AgentRunCommand(
request_id=uuid4(), expected_revision=published.task.active_run.revision,
source_session_id="session-1",
),
)
assert closed.status == "waiting_human"
assert closed.actions[0].action_id == action.action_id
assert [item["kind"] for item in repository.notifications] == [
"action_approval_pending"
]
@pytest.mark.anyio
async def test_rejected_action_requeues_task_in_a_new_execution_generation():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now).copy())
service = OperationalTaskService(
repository,
context,
{OPERATIONAL_ACTION_WORK_ORDER_APPROVE},
clock=lambda: now,
)
action = await service.propose_action(task["task_id"], ActionProposalCommand(
request_id=uuid4(), kind="create_work_order", reason="需要现场核验",
evidence_refs=[],
payload={"title": "现场核验", "description": "等待人工执行", "work_order_type": "sampling"},
))
original_request_id = repository.task["execution_request_id"]
rejected = await service.decide_action(
task["task_id"],
action.action_id,
ActionDecisionCommand(
request_id=uuid4(), expected_version=action.version,
decision="rejected", comment="当前方案不执行",
),
)
assert rejected.approval_status == "rejected"
assert rejected.version == action.version + 1
assert repository.task["status"] == "queued"
assert repository.task["execution_generation"] == 2
assert repository.task["execution_request_id"] != original_request_id
assert repository.task["agent_session_id"] is None
assert repository.task["agent_lease_expires_at"] is None
@pytest.mark.anyio
async def test_action_decision_rejects_a_stale_expected_version():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
service = OperationalTaskService(
repository,
context,
{OPERATIONAL_ACTION_WORK_ORDER_APPROVE},
clock=lambda: now,
)
action = await service.propose_action(task["task_id"], ActionProposalCommand(
request_id=uuid4(), kind="create_work_order", reason="需要现场核验",
evidence_refs=[],
payload={"title": "现场核验", "description": "等待人工执行", "work_order_type": "sampling"},
))
with pytest.raises(DomainError) as exc:
await service.decide_action(
task["task_id"],
action.action_id,
ActionDecisionCommand(
request_id=uuid4(), expected_version=action.version + 1,
decision="approved",
),
)
assert exc.value.code == "VERSION_CONFLICT"
assert repository.action["approval_status"] == "pending"
assert repository.action["version"] == action.version
@pytest.mark.anyio
async def test_new_payload_creates_a_new_pending_action_without_inheriting_approval():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = TaskRepository(task)
service = OperationalTaskService(
repository,
context,
{OPERATIONAL_ACTION_WORK_ORDER_APPROVE},
clock=lambda: now,
)
original = await service.propose_action(task["task_id"], ActionProposalCommand(
request_id=uuid4(), kind="create_work_order", reason="需要首次核验",
evidence_refs=[],
payload={"title": "首次核验", "description": "原始内容", "work_order_type": "sampling"},
))
approved = await service.decide_action(
task["task_id"],
original.action_id,
ActionDecisionCommand(
request_id=uuid4(), expected_version=original.version, decision="approved",
),
)
revised = await service.propose_action(task["task_id"], ActionProposalCommand(
request_id=uuid4(), kind="create_work_order", reason="内容已修改",
evidence_refs=[],
payload={"title": "修改后核验", "description": "新的内容", "work_order_type": "sampling"},
))
assert approved.approval_status == "approved"
assert revised.action_id != approved.action_id
assert revised.payload != approved.payload
assert revised.approval_status == "pending"
assert revised.execution_status == "not_submitted"
assert revised.version == 1
assert revised.decided_by is None
assert revised.decided_at is None
def _work_order_snapshot(work_order_id, now):
return {
"work_order_id": str(work_order_id),
"revision": 2,
"title": "复测预约",
"summary": "等待现场复测回令",
"status": "awaiting_reply",
"priority": "high",
"assignee": "水质班组",
"scheduled_at": (now + timedelta(hours=1)).isoformat(),
"work_order_type": "water_quality_complaint",
"location": "联调小区",
"scenario_code": "WQ-SECONDARY-SUPPLY-001",
"feedback_requirements": ["上传检测结果"],
"activity": [{"sequence": 1, "occurred_at": now.isoformat(), "actor": "水质班组",
"kind": "feedback", "title": "现场反馈", "detail": "上传复测结果"}],
"origin": "synthetic",
"updated_at": now.isoformat(),
}
@pytest.mark.anyio
async def test_external_feedback_exposes_validated_work_order_snapshot_and_refreshes_summary():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
work_order_id = uuid4()
task = _task_document(context, now, lease=None)
task.update(
task_type="external_work_order",
workflow="water_quality_complaint",
source={
"kind": "external_system",
"external_id": f"peripheral_sample:{work_order_id}",
"external_revision": 1,
"event_kind": "water_quality_complaint",
},
)
repository = TaskRepository(task)
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
result = await service.external_feedback(task["task_id"], ExternalFeedbackCommand(
request_id=uuid4(),
source_event_id=f"peripheral_sample:{work_order_id}",
source_revision=2,
status="awaiting_feedback",
occurred_at=now,
detail="等待现场复测回令",
work_order_snapshot=_work_order_snapshot(work_order_id, now),
))
assert result.task.status == "waiting_human"
assert result.task.title == "复测预约"
assert result.task.summary == "等待现场复测回令"
assert result.task.scheduled_for == now + timedelta(hours=1)
assert result.task.priority == "urgent"
assert result.task.work_order_snapshot is not None
assert result.task.work_order_snapshot.activity[0].title == "现场反馈"
assert result.task.work_order_snapshot.feedback_requirements == ["上传检测结果"]
@pytest.mark.anyio
async def test_event_investigation_rejects_work_order_snapshot():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = TaskRepository(task)
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
work_order_id = uuid4()
with pytest.raises(DomainError) as exc:
await service.external_feedback(task["task_id"], ExternalFeedbackCommand(
request_id=uuid4(), source_event_id=str(work_order_id), source_revision=2,
status="awaiting_feedback", occurred_at=now, detail="不应接受",
work_order_snapshot=_work_order_snapshot(work_order_id, now),
))
assert exc.value.code == "INVALID_FEEDBACK_SNAPSHOT"
@pytest.mark.anyio
async def test_expired_owned_lease_is_rejected_with_stable_code():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now - timedelta(seconds=1))
repository = RunTaskRepository(task, _active_run(task, now))
repository.run["lease_expires_at"] = now - timedelta(seconds=1)
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
with pytest.raises(DomainError) as exc:
await service.heartbeat_run(
task["task_id"], repository.run["run_id"],
AgentRunCommand(
request_id=uuid4(), expected_revision=2, source_session_id="session-1"
),
)
assert exc.value.code == "LEASE_EXPIRED"
assert repository.run["revision"] == 2
@pytest.mark.anyio
async def test_interrupted_submit_replays_unknown_without_dispatching_twice():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
task.update(status="waiting_human", agent_lease_expires_at=None)
action = HumanApprovalAction(
action_id=uuid4(),
task_id=task["task_id"],
version=2,
kind="create_work_order",
reason="现场核验",
payload={"title": "采样", "description": "核验", "work_order_type": "sampling"},
evidence_refs=[],
required_approval_permission="work-order.approve",
required_execution_permission=OPERATIONAL_ACTION_WORK_ORDER_EXECUTE,
approval_status="approved",
execution_status="not_submitted",
proposed_by="principal:agent",
proposed_at=now,
decided_by="principal:approver",
decided_at=now,
).model_dump(mode="json")
repository = TaskRepository(task, action=action)
service = OperationalTaskService(
repository,
context,
{OPERATIONAL_ACTION_WORK_ORDER_EXECUTE},
clock=lambda: now,
)
command = ActionExecuteCommand(request_id=uuid4(), expected_version=2)
class LostResponseDispatcher:
def __init__(self):
self.calls = 0
async def dispatch(self, **_kwargs):
self.calls += 1
raise RuntimeError("connection lost after submission")
dispatcher = LostResponseDispatcher()
with pytest.raises(RuntimeError):
await service.submit_action(
task["task_id"], action["action_id"], command, dispatcher, None
)
replay = await service.submit_action(
task["task_id"], action["action_id"], command, dispatcher, None
)
assert dispatcher.calls == 1
assert replay.execution_status == "unknown"
assert replay.execution_request_id is not None
assert [item["kind"] for item in repository.notifications] == [
"external_coordination_failed"
]
@pytest.mark.anyio
async def test_snapshot_reuse_returns_the_original_provenance_and_rejects_collision():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = TaskRepository(task)
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
original = TaskDataSnapshot(
source="peripheral_points",
source_ref="peripheral_points:sha256:same",
queried_at=now,
origin="synthetic",
data={"items": [{"id": "p-1"}]},
truncated=False,
)
evidence, stored = await service.record_data_snapshot(task["task_id"], original)
replayed = original.model_copy(update={"queried_at": now + timedelta(minutes=1)})
replay_evidence, replay_snapshot = await service.record_data_snapshot(
task["task_id"], replayed
)
assert replay_evidence == evidence
assert replay_snapshot == stored == original
collision = original.model_copy(update={"data": {"items": [{"id": "p-2"}]}})
with pytest.raises(DomainError) as exc:
await service.record_data_snapshot(task["task_id"], collision)
assert exc.value.code == "EVIDENCE_CONFLICT"
@pytest.mark.anyio
async def test_repository_task_update_rejects_a_stale_revision():
cursor = SimpleNamespace(rowcount=0)
connection = SimpleNamespace(execute=AsyncMock(return_value=cursor))
repository = OperationalTaskRepository(connection, uuid4())
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
task["revision"] = 3
with pytest.raises(DomainError) as exc:
await repository.update_task(task)
assert exc.value.code == "VERSION_CONFLICT"