feat(workbench): persist operational workflows and events

This commit is contained in:
2026-09-24 04:19:02 +08:00
parent baff7dfe9e
commit 9335bdc1ea
66 changed files with 16580 additions and 238 deletions
+852
View File
@@ -0,0 +1,852 @@
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
@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
@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
@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",
"subtasks": [{
"id": "sampling",
"title": "现场采样",
"summary": "采集并复测",
"status": "running",
"scheduled_at": (now + timedelta(hours=1)).isoformat(),
"location": "联调小区",
"assignee": "水质班组",
"duration_minutes": 60,
"reply_window_minutes": 120,
"stages": [{
"id": "reply",
"sequence": 1,
"title": "等待回令",
"status": "current",
"owner": "水质班组",
"time_label": "待反馈",
"description": "上传复测结果",
}],
"reply_requirements": ["上传检测结果"],
}],
"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.subtasks[0].stages[0].status == "current"
@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
@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"