feat(investigations): add event impact and work-order feedback flows
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.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user