Add isolated burst and pollution impact evidence, parallel work-order workflows, persisted notifications and graph updates. Checkpoint current code. Known follow-ups remain for large pollution result previews and graceful geometry failure.
140 lines
9.8 KiB
Python
140 lines
9.8 KiB
Python
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)
|