feat: extend operational investigation workflows
Generic Container CI/CD / test-build-publish (push) Successful in 3m53s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent 63af998045
commit 05183aff93
82 changed files with 4644 additions and 330 deletions
+23 -34
View File
@@ -14,7 +14,6 @@ from scripts.sync_peripheral_work_orders import (
sync_once,
work_order_snapshot,
)
from samples.peripheral.seed_identity import SEED_ACTOR_ID
from app.domain.schemas.operational_task import ExternalFeedbackCommand
@@ -36,27 +35,21 @@ def test_documented_sync_command_can_start_from_repository_root():
assert "--once" in result.stdout
def test_unlinked_synthetic_checks_are_not_operational_tasks():
def test_all_active_work_orders_are_operational_tasks():
order_id = str(uuid4())
order = {
"work_order_id": order_id,
"origin": "synthetic",
"created_by": str(SEED_ACTOR_ID),
"title": "合成压力偏差复核",
"origin": "system_data",
"title": "压力偏差复核",
"work_order_type": "general",
}
assert not should_project_work_order(order, set())
assert should_project_work_order(order, set())
assert should_project_work_order(order, {order_id})
assert should_project_work_order(
{**order, "work_order_type": "water_quality_complaint"}, set()
)
assert should_project_work_order(
{**order, "title": "现场采样:二次供水出水及末端用户水样"}, set()
) is False
assert should_project_work_order(
{**order, "created_by": str(uuid4()), "title": "合成管材接驳预约"}, set()
)
assert should_project_work_order({**order, "title": "现场采样"}, set())
def test_peripheral_order_id_ignores_foreign_and_malformed_namespaces():
@@ -71,7 +64,7 @@ def test_peripheral_order_id_ignores_foreign_and_malformed_namespaces():
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",
order = {"project_id": project_id, "work_order_id": order_id, "origin": "system_data",
"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"}
@@ -116,16 +109,15 @@ async def test_sync_creates_one_task_and_only_applies_new_external_revisions():
@pytest.mark.anyio
async def test_sync_skips_unlinked_synthetic_check_before_writing():
async def test_sync_rejects_non_system_work_order_origin():
project_id, order_id = str(uuid4()), str(uuid4())
order = {
"project_id": project_id,
"work_order_id": order_id,
"origin": "synthetic",
"created_by": str(SEED_ACTOR_ID),
"revision": 1,
"title": "合成通信核查",
"summary": "仅用于外围样板联调",
"title": "来源异常工单",
"summary": "验证来源边界",
"priority": "normal",
"status": "open",
"created_at": "2026-09-23T08:00:00Z",
@@ -135,7 +127,6 @@ async def test_sync_skips_unlinked_synthetic_check_before_writing():
}
def handler(request):
assert request.method == "GET"
if request.url.host == "sample.test":
return httpx.Response(200, json={"items": [order], "next_cursor": None})
return httpx.Response(200, json={"items": [], "next_offset": None})
@@ -144,14 +135,8 @@ async def test_sync_skips_unlinked_synthetic_check_before_writing():
"http://server.test", "http://agent.test", project_id, bearer="TEST_ONLY"
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
synchronized = await sync_once(
client,
settings,
"http://sample.test",
CredentialProvider(settings),
)
assert synchronized == 0
with pytest.raises(ValueError, match="origin mismatch"):
await sync_once(client, settings, "http://sample.test", CredentialProvider(settings))
@pytest.mark.anyio
@@ -231,7 +216,7 @@ async def test_sync_does_not_withdraw_when_missing_order_is_not_confirmed_delete
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"}]
return httpx.Response(200, json={"items": ([{"project_id": str(uuid4()), "origin": "system_data"}]
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:
@@ -263,11 +248,11 @@ async def test_created_work_order_receipt_preserves_projection_and_routes_active
def handler(request):
if request.url.host == "sample.test":
order = {
"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
"project_id": project_id, "work_order_id": order_id, "origin": "system_data",
"created_by": str(uuid4()),
"revision": order_revision, "summary": "采样任务", "status": "completed" if order_revision > 1 else "open",
"updated_at": "2026-09-23T09:00:00Z",
"title": "合成采样工单", "priority": "normal",
"title": "采样工单", "priority": "normal",
"work_order_type": "general", "created_at": "2026-09-23T08:00:00Z",
"scheduled_at": None,
"activity": ([{"sequence": 2, "occurred_at": "2026-09-23T09:00:00Z", "actor": "现场人员",
@@ -320,13 +305,15 @@ async def test_external_events_start_investigations_without_browser(event_kind):
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,
"project_id": project_id, "work_order_id": order_id, "origin": "system_data", "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"):
assert payload["evidence"]["kind"] == "external_feedback"
assert payload["work_order_snapshot"]["origin"] == "system_data"
return httpx.Response(200, json={})
task = {**payload, "task_id": str(uuid4())}
created.append(task)
@@ -339,6 +326,7 @@ async def test_external_events_start_investigations_without_browser(event_kind):
assert investigation["workflow"] == event_kind
assert investigation["source"]["external_id"] == order_id
assert investigation["trigger"]["occurred_at"] == "2026-09-23T07:30:00Z"
assert all("合成" not in item["summary"] for item in created)
@pytest.mark.anyio
@@ -349,7 +337,7 @@ async def test_child_feedback_retry_keeps_projection_cursor_behind_all_parent_de
projection = {"task_id": projection_id, "task_type": "external_work_order", "source": {
"external_id": f"peripheral_sample:{order_id}", "external_revision": 1,
}}
order = {"project_id": project_id, "work_order_id": order_id, "origin": "synthetic",
order = {"project_id": project_id, "work_order_id": order_id, "origin": "system_data",
"revision": 2, "title": "采样", "summary": "复测通过", "status": "completed", "priority": "normal",
"created_at": "2026-10-10T00:00:00Z", "updated_at": "2026-10-10T01:00:00Z", "scheduled_at": None,
"work_order_type": "general"}
@@ -398,8 +386,8 @@ async def test_child_feedback_retry_keeps_projection_cursor_behind_all_parent_de
def test_snapshot_preserves_recorded_activity_without_legacy_subtasks():
order = {
"work_order_id": str(uuid4()), "revision": 4, "title": "合成预约", "summary": "仅测试过程同步",
"status": "in_progress", "priority": "normal", "origin": "synthetic",
"work_order_id": str(uuid4()), "revision": 4, "title": "预约工单", "summary": "仅测试过程同步",
"status": "in_progress", "priority": "normal", "origin": "system_data",
"updated_at": "2026-09-23T09:00:00Z", "work_order_type": "general",
"feedback_requirements": ["回填现场检查结果"],
"activity": [{
@@ -411,7 +399,7 @@ def test_snapshot_preserves_recorded_activity_without_legacy_subtasks():
"detail": "已创建现场检查工单。",
}],
"subtasks": [{
"id": "visit", "title": "现场检查", "summary": "合成测试,未现场执行",
"id": "visit", "title": "现场检查", "summary": "测试数据,未现场执行",
"status": "pending", "scheduled_at": "2026-09-24T08:00:00Z",
"location": "测试位置", "assignee": "测试班组", "duration_minutes": 30,
"reply_window_minutes": 15, "reply_requirements": ["检查记录"],
@@ -420,6 +408,7 @@ def test_snapshot_preserves_recorded_activity_without_legacy_subtasks():
}],
}
snapshot = work_order_snapshot(order)
assert snapshot["origin"] == "system_data"
assert snapshot["feedback_requirements"] == ["回填现场检查结果"]
assert snapshot["activity"] == order["activity"]
assert "subtasks" not in snapshot