from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock from uuid import uuid4 import pytest from app.domain.domain_errors import DomainError from app.domain.schemas.operational_data import TaskDataSnapshot from app.domain.schemas.operational_task import ( ActionDecisionCommand, ActionExecuteCommand, ActionProposalCommand, ActionReceipt, AgentArtifactsCommand, AgentRunCommand, ExternalFeedbackCommand, ) from app.services.operational_recovery import track_work_order from app.services.operational_tasks import OperationalTaskService from tests.support.operational_task_repository import TaskRepository, _active_run, _context, _task_document @pytest.mark.anyio async def test_pollution_work_order_can_be_approved_and_dispatched_while_simulation_runs(): from types import SimpleNamespace from app.auth.permissions import OPERATIONAL_ACTION_WORK_ORDER_APPROVE, OPERATIONAL_ACTION_WORK_ORDER_EXECUTE from app.infra.integrations.operational_actions import ActionDispatchOutcome now = datetime(2026, 7, 16, 6, tzinfo=timezone.utc) context = _context() task = _task_document(context, now, lease=now + timedelta(minutes=1)) task["workflow"] = "water_pollution" repository = TaskRepository(task, _active_run(task, now)) repository.pending_calculation_jobs = AsyncMock(return_value=["pollution-simulation"]) service = OperationalTaskService(repository, context, set(), clock=lambda: now) report = {"stage": "interim", "outcome": "concluded", "data_gaps": [], "title": "候选源核验", "current_conclusion": "候选节点已取得,可先派现场核查", "conclusion_boundary": "污染传播计算中", "evidence_refs": [], "recommendations": ["采样核验"], "pending_items": ["传播模拟"]} await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report=report)) action = await service.propose_action(task["task_id"], ActionProposalCommand( request_id=uuid4(), kind="create_work_order", reason="先核验候选源", payload={"title": "候选节点现场核查", "description": "核查污染进入路径并采样", "work_order_type": "general"})) with pytest.raises(DomainError) as denied: await service.decide_action(task["task_id"], action.action_id, ActionDecisionCommand( request_id=uuid4(), expected_version=action.version, decision="approved")) assert denied.value.code == "PERMISSION_DENIED" service = OperationalTaskService(repository, context, {OPERATIONAL_ACTION_WORK_ORDER_APPROVE, OPERATIONAL_ACTION_WORK_ORDER_EXECUTE}, clock=lambda: now) approved = await service.decide_action(task["task_id"], action.action_id, ActionDecisionCommand( request_id=uuid4(), expected_version=action.version, decision="approved")) dispatcher = SimpleNamespace(dispatch=AsyncMock(return_value=ActionDispatchOutcome("succeeded", "现场工单已创建", ActionReceipt(adapter="peripheral_sample", receipt_id=str(uuid4()), submitted_at=now, completed_at=now, external_status="open", detail="创建成功")))) with pytest.raises(DomainError) as stale: await service.submit_action(task["task_id"], action.action_id, ActionExecuteCommand( request_id=uuid4(), expected_version=action.version), dispatcher, None) assert stale.value.code == "VERSION_CONFLICT" dispatched = await service.submit_action(task["task_id"], action.action_id, ActionExecuteCommand( request_id=uuid4(), expected_version=approved.version), dispatcher, None) assert dispatched.execution_status == "succeeded" assert repository.task["handling_context"]["phase"] == "waiting_work_order" assert repository.run["status"] == "running" and repository.task["execution_generation"] == 1 dispatcher.dispatch.assert_awaited_once() repository.pending_calculation_jobs.assert_not_awaited() # No simulation dependency on dispatch. with pytest.raises(DomainError) as pending: await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report={**report, "stage": "final"})) assert pending.value.code == "PENDING_CALCULATIONS" repository.pending_calculation_jobs.return_value = [] evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot( source="run_contaminant_simulation", source_ref="calculation:pollution", queried_at=now, origin="synthetic", truncated=False, data={"result": {"impact": {"affected_node_count": 2}}})) await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report={**report, "current_conclusion": "传播模拟完成,现场核查继续", "evidence_refs": [evidence.evidence_id], "pending_items": [f"等待现场工单反馈,action_id={action.action_id}"]})) assert len(repository.reports) == 2 and repository.action["version"] == dispatched.version assert repository.action["approval_status"] == "approved" and repository.run["status"] == "running" @pytest.fixture def anyio_backend(): return "asyncio" @pytest.mark.anyio async def test_pollution_report_waits_for_field_feedback_and_requires_latest_feedback_evidence(monkeypatch): now = datetime(2026, 7, 16, 6, tzinfo=timezone.utc) context = _context() task = _task_document(context, now, lease=now + timedelta(minutes=1)) task.update(workflow="water_pollution", source={"kind": "external_system", "external_id": "scada:quality:1", "external_revision": 1, "event_kind": "water_pollution"}) repository = TaskRepository(task, _active_run(task, now)) service = OperationalTaskService(repository, context, set(), clock=lambda: now) monkeypatch.setattr(service, "_resolve_investigation_alerts", AsyncMock()) scada, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot( source="query_scada_readings", source_ref="mcp:quality", queried_at=now, origin="system_data", truncated=False, data={ "window": {"start": (now - timedelta(hours=3)).isoformat(), "end": now.isoformat()}, "data": {"devices": [], "readings": [], "coverage": {"mode": "all_scada", "query_complete": True}}, })) report = {"stage": "interim", "outcome": "concluded", "data_gaps": [], "title": "水污染事件调查", "current_conclusion": "候选分析完成,待采样与处置反馈", "conclusion_boundary": "未确诊污染源", "evidence_refs": [scada.evidence_id], "recommendations": ["现场采样复测"], "pending_items": ["现场反馈"]} await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report=report)) action = await service.propose_action(task["task_id"], ActionProposalCommand( request_id=uuid4(), kind="create_work_order", reason="采样和处置核验", payload={"title": "节点采样复测", "description": "回传位置、时间、数值和判定依据", "work_order_type": "general"})) child = str(uuid4()) repository.action = action.model_copy(update={"approval_status": "approved", "execution_status": "succeeded"}).model_dump(mode="json") repository.action["receipt"] = {"adapter": "peripheral_sample", "receipt_id": child, "submitted_at": now.isoformat(), "completed_at": now.isoformat(), "external_status": "open", "detail": "创建成功"} track_work_order(repository.task, repository.action) result = 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 result.status == "waiting_human" service._resolve_investigation_alerts.assert_not_awaited() evidence_id = "field-quality:2" await service.external_feedback(task["task_id"], ExternalFeedbackCommand( request_id=uuid4(), source_event_id=f"peripheral_sample:{child}", source_revision=2, status="completed", occurred_at=now, detail="采样、处置与复测反馈已返回", evidence={"evidence_id": evidence_id, "kind": "external_feedback", "summary": "现场记录及复测结果", "source_label": "现场反馈", "source_ref": f"peripheral_sample:{child}", "observed_at": now})) assert repository.task["handling_context"]["phase"] == "field_review" assert "last_check" not in repository.task["handling_context"] # No pressure recovery shortcut. await service.claim_run(task["task_id"], repository.run["run_id"], AgentRunCommand( request_id=repository.run["execution_request_id"], expected_revision=repository.run["revision"], source_session_id="session-1")) await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report={**report, "stage": "final"})) with pytest.raises(DomainError) as rejected: 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 rejected.value.code == "POLLUTION_FEEDBACK_REQUIRED" await service.publish_artifacts(task["task_id"], AgentArtifactsCommand( request_id=uuid4(), source_session_id="session-1", report={**report, "stage": "final", "evidence_refs": [scada.evidence_id, evidence_id], "pending_items": []})) result = 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 result.status == "completed" and result.handling_context["phase"] == "completed" service._resolve_investigation_alerts.assert_awaited_once_with(task["task_id"], evidence_id)