163 lines
9.3 KiB
Python
163 lines
9.3 KiB
Python
import json
|
|
from uuid import uuid4
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from scripts.run_operational_worker import CredentialProvider, WorkerSettings
|
|
from scripts.sync_peripheral_work_orders import sync_once, work_order_snapshot
|
|
from app.domain.schemas.operational_task import ExternalFeedbackCommand
|
|
|
|
|
|
@pytest.fixture
|
|
def anyio_backend():
|
|
return "asyncio"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_sync_creates_one_task_and_only_applies_new_external_revisions():
|
|
project_id, order_id, task_id = map(str, (uuid4(), uuid4(), uuid4()))
|
|
tasks, writes = [], []
|
|
order = {"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
|
|
"revision": 1, "title": "水表维修", "summary": "等待现场班组处理", "priority": "normal",
|
|
"status": "open", "created_at": "2026-09-23T08:00:00Z", "updated_at": "2026-09-23T08:00:00Z",
|
|
"scheduled_at": None, "work_order_type": "general"}
|
|
def handler(request):
|
|
if request.url.host == "sample.test":
|
|
return httpx.Response(200, json={"items": [order], "next_cursor": None})
|
|
if request.method == "GET":
|
|
return httpx.Response(200, json={"items": tasks, "next_offset": None})
|
|
body = json.loads(request.content)
|
|
writes.append((request.url.path, body))
|
|
if request.url.path.endswith("external-feedback"):
|
|
tasks[0]["source"]["external_revision"] = body["source_revision"]
|
|
tasks[0]["status"] = body["status"]
|
|
return httpx.Response(200, json={"task": tasks[0]})
|
|
task = {**body, "task_id": task_id}
|
|
tasks.append(task)
|
|
return httpx.Response(201, json=task)
|
|
settings = WorkerSettings("http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY")
|
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
|
credentials = CredentialProvider(settings)
|
|
assert await sync_once(client, settings, "http://sample.test", credentials) == 1
|
|
assert await sync_once(client, settings, "http://sample.test", credentials) == 0
|
|
order.update(revision=2, status="awaiting_reply", updated_at="2026-09-23T09:00:00Z",
|
|
title="水表维修复核", assignee="测试班组", location="测试位置",
|
|
scheduled_at="2026-09-24T08:00:00Z")
|
|
assert await sync_once(client, settings, "http://sample.test", credentials) == 1
|
|
assert len(tasks) == 1
|
|
assert len(writes) == 3
|
|
assert writes[-1][1]["status"] == "awaiting_feedback"
|
|
assert writes[-1][1]["source_revision"] == 2
|
|
snapshot = writes[-1][1]["work_order_snapshot"]
|
|
assert snapshot["status"] == "awaiting_reply"
|
|
assert snapshot["title"] == "水表维修复核"
|
|
assert snapshot["assignee"] == "测试班组"
|
|
assert snapshot["location"] == "测试位置"
|
|
assert snapshot["scheduled_at"] == "2026-09-24T08:00:00Z"
|
|
assert snapshot["subtasks"] is None
|
|
# Exercise the real API schema, including the prefixed external identity.
|
|
accepted = ExternalFeedbackCommand.model_validate(writes[-1][1])
|
|
assert str(accepted.work_order_snapshot.work_order_id) == order_id
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_sample_sync_rejects_cross_project_data_before_writing():
|
|
def handler(request):
|
|
assert request.method == "GET"
|
|
return httpx.Response(200, json={"items": ([{"project_id": str(uuid4()), "origin": "synthetic"}]
|
|
if request.url.host == "sample.test" else []), "next_offset": None})
|
|
settings = WorkerSettings("http://server.test", "http://agent.test", str(uuid4()), bearer="TEST_ONLY")
|
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
|
with pytest.raises(ValueError, match="scope"):
|
|
await sync_once(client, settings, "http://sample.test", CredentialProvider(settings))
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize("investigation_status", ["waiting_human", "queued"])
|
|
async def test_created_work_order_receipt_routes_feedback_back_to_investigation(investigation_status):
|
|
project_id, order_id, investigation_id, projection_id = map(str, (uuid4(), uuid4(), uuid4(), uuid4()))
|
|
received = []
|
|
def handler(request):
|
|
if request.url.host == "sample.test":
|
|
return httpx.Response(200, json={"items": [{
|
|
"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
|
|
"revision": 2, "summary": "采样已完成", "status": "completed", "updated_at": "2026-09-23T09:00:00Z",
|
|
"title": "合成采样工单", "priority": "normal",
|
|
"work_order_type": "water_quality_complaint", "created_at": "2026-09-23T08:00:00Z",
|
|
"scheduled_at": None,
|
|
}], "next_cursor": None})
|
|
if request.method == "POST":
|
|
assert request.url.path.endswith("external-feedback"), "Linked child orders must not start another investigation"
|
|
received.append(request.url.path)
|
|
payload = json.loads(request.content)
|
|
assert payload["source_event_id"] == f"peripheral_sample:{order_id}"
|
|
assert ("work_order_snapshot" in payload) == request.url.path.endswith(f"{projection_id}/external-feedback")
|
|
return httpx.Response(200, json={})
|
|
if request.url.path.endswith(investigation_id):
|
|
return httpx.Response(200, json={"actions": [{"kind": "create_work_order", "receipt": {
|
|
"adapter": "peripheral_sample", "receipt_id": order_id,
|
|
}}]})
|
|
return httpx.Response(200, json={"items": [
|
|
{"task_id": investigation_id, "task_type": "event_investigation", "status": investigation_status},
|
|
{"task_id": projection_id, "task_type": "external_work_order", "source": {
|
|
"external_id": f"peripheral_sample:{order_id}", "external_revision": 1,
|
|
}},
|
|
], "next_offset": None})
|
|
settings = WorkerSettings("http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY")
|
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
|
assert await sync_once(client, settings, "http://sample.test", CredentialProvider(settings)) == 2
|
|
assert any(investigation_id in path for path in received)
|
|
assert any(projection_id in path for path in received)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize("event_kind", ["water_quality_complaint", "burst_report"])
|
|
async def test_external_events_start_investigations_without_browser(event_kind):
|
|
project_id, order_id = str(uuid4()), str(uuid4())
|
|
created = []
|
|
def handler(request):
|
|
if request.method == "GET" and request.url.host == "server.test":
|
|
return httpx.Response(200, json={"items": [], "next_offset": None})
|
|
if request.url.host == "sample.test":
|
|
return httpx.Response(200, json={"items": [{
|
|
"project_id": project_id, "work_order_id": order_id, "origin": "synthetic", "revision": 1,
|
|
"title": "外围测试事件", "summary": "模拟外部系统上报", "priority": "urgent", "status": "open",
|
|
"created_at": "2026-09-23T08:00:00Z", "updated_at": "2026-09-23T08:00:00Z",
|
|
"scheduled_at": "2026-09-23T07:30:00Z", "work_order_type": event_kind, "location": "测试位置",
|
|
}], "next_cursor": None})
|
|
payload = json.loads(request.content)
|
|
if request.url.path.endswith("external-feedback"):
|
|
return httpx.Response(200, json={})
|
|
task = {**payload, "task_id": str(uuid4())}
|
|
created.append(task)
|
|
return httpx.Response(201, json=task)
|
|
settings = WorkerSettings("http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY")
|
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
|
await sync_once(client, settings, "http://sample.test", CredentialProvider(settings))
|
|
assert {task["task_type"] for task in created} == {"event_investigation", "external_work_order"}
|
|
investigation = next(task for task in created if task["task_type"] == "event_investigation")
|
|
assert investigation["workflow"] == event_kind
|
|
assert investigation["source"]["external_id"] == order_id
|
|
assert investigation["trigger"]["occurred_at"] == "2026-09-23T07:30:00Z"
|
|
|
|
|
|
def test_snapshot_preserves_nested_process_without_inventing_completed_stages():
|
|
order = {
|
|
"work_order_id": str(uuid4()), "revision": 4, "title": "合成预约", "summary": "仅测试过程同步",
|
|
"status": "in_progress", "priority": "normal", "origin": "synthetic",
|
|
"updated_at": "2026-09-23T09:00:00Z", "work_order_type": "general",
|
|
"subtasks": [{
|
|
"id": "visit", "title": "现场检查", "summary": "合成测试,未现场执行",
|
|
"status": "pending", "scheduled_at": "2026-09-24T08:00:00Z",
|
|
"location": "测试位置", "assignee": "测试班组", "duration_minutes": 30,
|
|
"reply_window_minutes": 15, "reply_requirements": ["检查记录"],
|
|
"stages": [{"id": "dispatch", "sequence": 1, "title": "派发", "status": "pending",
|
|
"owner": "测试调度", "time_label": "待安排", "description": "尚未派发"}],
|
|
}],
|
|
}
|
|
snapshot = work_order_snapshot(order)
|
|
assert snapshot["subtasks"] == order["subtasks"]
|
|
assert snapshot["subtasks"][0]["stages"][0]["status"] == "pending"
|
|
assert work_order_snapshot({key: value for key, value in order.items() if key != "subtasks"})["subtasks"] is None
|