fix(workbench): remove synthetic check projections

This commit is contained in:
2026-09-24 12:01:01 +08:00
parent 1a6ff64bbe
commit e374b5b3cb
25 changed files with 953 additions and 935 deletions
+5 -22
View File
@@ -647,27 +647,9 @@ def _work_order_snapshot(work_order_id, now):
"work_order_type": "water_quality_complaint",
"location": "联调小区",
"scenario_code": "WQ-SECONDARY-SUPPLY-001",
"subtasks": [{
"id": "sampling",
"title": "现场采样",
"summary": "采集并复测",
"status": "running",
"scheduled_at": (now + timedelta(hours=1)).isoformat(),
"location": "联调小区",
"assignee": "水质班组",
"duration_minutes": 60,
"reply_window_minutes": 120,
"stages": [{
"id": "reply",
"sequence": 1,
"title": "等待回令",
"status": "current",
"owner": "水质班组",
"time_label": "待反馈",
"description": "上传复测结果",
}],
"reply_requirements": ["上传检测结果"],
}],
"feedback_requirements": ["上传检测结果"],
"activity": [{"sequence": 1, "occurred_at": now.isoformat(), "actor": "水质班组",
"kind": "feedback", "title": "现场反馈", "detail": "上传复测结果"}],
"origin": "synthetic",
"updated_at": now.isoformat(),
}
@@ -707,7 +689,8 @@ async def test_external_feedback_exposes_validated_work_order_snapshot_and_refre
assert result.task.scheduled_for == now + timedelta(hours=1)
assert result.task.priority == "urgent"
assert result.task.work_order_snapshot is not None
assert result.task.work_order_snapshot.subtasks[0].stages[0].status == "current"
assert result.task.work_order_snapshot.activity[0].title == "现场反馈"
assert result.task.work_order_snapshot.feedback_requirements == ["上传检测结果"]
@pytest.mark.anyio
@@ -25,7 +25,7 @@ class ManagedStore:
return command.model_dump(mode="json", exclude={"request_id"}) | {
"work_order_id": str(uuid4()), "project_id": str(PROJECT_ID), "revision": 1,
"origin": "synthetic", "created_at": now, "updated_at": now,
"created_by": str(USER_ID), "updated_by": str(USER_ID),
"created_by": str(USER_ID), "updated_by": str(USER_ID), "activity": [],
}
def update_work_order(self, who, work_order_id, command):
@@ -35,7 +35,7 @@ class ManagedStore:
"status": command.status or "open", "priority": command.priority or "normal",
"assignee": None, "scheduled_at": None, "work_order_type": "general",
"location": command.location, "scenario_code": command.scenario_code,
"subtasks": command.subtasks}})()) | {"revision": command.expected_revision + 1,
"feedback_requirements": command.feedback_requirements or []}})()) | {"revision": command.expected_revision + 1,
"work_order_id": str(work_order_id)}
def delete_work_order(self, who, work_order_id, command):
@@ -86,32 +86,25 @@ def test_managed_work_order_and_data_routes_are_typed_crud():
"status": "open", "priority": "high", "assignee": None, "scheduled_at": None,
"work_order_type": "water_quality_complaint", "location": "演示小区",
"scenario_code": "WQ-SECONDARY-SUPPLY-001",
"subtasks": [{
"id": "sampling", "title": "现场采样", "summary": "采集二次供水样本",
"status": "running", "scheduled_at": "2026-09-18T01:00:00Z",
"location": "演示小区", "assignee": "水质班组", "duration_minutes": 60,
"reply_window_minutes": 120, "reply_requirements": ["上传检测结果"],
"stages": [{"id": "dispatch", "sequence": 1, "title": "派发",
"status": "current", "owner": "调度员", "time_label": "待执行",
"description": "确认采样人员"}],
}],
"feedback_requirements": ["上传检测结果"],
})
assert created.status_code == 201 and created.json()["origin"] == "synthetic"
assert created.json()["work_order_type"] == "water_quality_complaint"
assert created.json()["location"] == "演示小区"
assert created.json()["scenario_code"] == "WQ-SECONDARY-SUPPLY-001"
assert created.json()["subtasks"][0]["stages"][0]["status"] == "current"
assert created.json()["feedback_requirements"] == ["上传检测结果"]
assert "subtasks" not in created.json()
work_order_id = created.json()["work_order_id"]
changed = client.patch(f"/api/sample/v1/work-orders/{work_order_id}", json={
"request_id": str(uuid4()), "expected_revision": 1, "status": "awaiting_reply",
"location": "复测点", "clear_scenario_code": True, "clear_subtasks": True,
"location": "复测点", "clear_scenario_code": True, "progress_note": "等待现场检测结果",
"clear_assignee": False, "clear_scheduled_at": False,
})
assert changed.status_code == 200 and changed.json()["revision"] == 2
assert changed.json()["status"] == "awaiting_reply"
update_command = next(call[3] for call in fake.calls if call[0] == "update_work_order")
assert update_command.location == "复测点"
assert update_command.clear_scenario_code and update_command.clear_subtasks
assert update_command.clear_scenario_code and update_command.progress_note == "等待现场检测结果"
deleted = client.request("DELETE", f"/api/sample/v1/work-orders/{work_order_id}", json={
"request_id": str(uuid4()), "expected_revision": 2,
})
+102 -11
View File
@@ -5,7 +5,12 @@ 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 scripts.sync_peripheral_work_orders import (
should_project_work_order,
sync_once,
work_order_snapshot,
)
from samples.peripheral.seed_identity import SEED_ACTOR_ID
from app.domain.schemas.operational_task import ExternalFeedbackCommand
@@ -14,6 +19,29 @@ def anyio_backend():
return "asyncio"
def test_unlinked_synthetic_checks_are_not_operational_tasks():
order_id = str(uuid4())
order = {
"work_order_id": order_id,
"origin": "synthetic",
"created_by": str(SEED_ACTOR_ID),
"title": "合成压力偏差复核",
"work_order_type": "general",
}
assert not 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()
)
@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()))
@@ -55,12 +83,52 @@ async def test_sync_creates_one_task_and_only_applies_new_external_revisions():
assert snapshot["assignee"] == "测试班组"
assert snapshot["location"] == "测试位置"
assert snapshot["scheduled_at"] == "2026-09-24T08:00:00Z"
assert snapshot["subtasks"] is None
assert snapshot["feedback_requirements"] == []
assert snapshot["activity"] == []
# 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_sync_skips_unlinked_synthetic_check_before_writing():
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": "仅用于外围样板联调",
"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):
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})
settings = WorkerSettings(
"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
@pytest.mark.anyio
async def test_sample_sync_rejects_cross_project_data_before_writing():
def handler(request):
@@ -74,17 +142,29 @@ async def test_sample_sync_rejects_cross_project_data_before_writing():
@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):
@pytest.mark.parametrize(
("investigation_status", "expected_feedback_count", "routes_to_investigation"),
[
("waiting_human", 2, True),
("queued", 2, True),
("completed", 1, False),
],
)
async def test_created_work_order_receipt_preserves_projection_and_routes_active_feedback(
investigation_status,
expected_feedback_count,
routes_to_investigation,
):
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",
"created_by": str(uuid4()),
"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",
"work_order_type": "general", "created_at": "2026-09-23T08:00:00Z",
"scheduled_at": None,
}], "next_cursor": None})
if request.method == "POST":
@@ -106,8 +186,10 @@ async def test_created_work_order_receipt_routes_feedback_back_to_investigation(
], "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 await sync_once(
client, settings, "http://sample.test", CredentialProvider(settings)
) == expected_feedback_count
assert any(investigation_id in path for path in received) is routes_to_investigation
assert any(projection_id in path for path in received)
@@ -142,11 +224,20 @@ async def test_external_events_start_investigations_without_browser(event_kind):
assert investigation["trigger"]["occurred_at"] == "2026-09-23T07:30:00Z"
def test_snapshot_preserves_nested_process_without_inventing_completed_stages():
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",
"updated_at": "2026-09-23T09:00:00Z", "work_order_type": "general",
"feedback_requirements": ["回填现场检查结果"],
"activity": [{
"sequence": 1,
"occurred_at": "2026-09-23T08:00:00Z",
"actor": "测试调度",
"kind": "created",
"title": "工单创建",
"detail": "已创建现场检查工单。",
}],
"subtasks": [{
"id": "visit", "title": "现场检查", "summary": "合成测试,未现场执行",
"status": "pending", "scheduled_at": "2026-09-24T08:00:00Z",
@@ -157,6 +248,6 @@ def test_snapshot_preserves_nested_process_without_inventing_completed_stages():
}],
}
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
assert snapshot["feedback_requirements"] == ["回填现场检查结果"]
assert snapshot["activity"] == order["activity"]
assert "subtasks" not in snapshot
@@ -0,0 +1,49 @@
from uuid import uuid4
from samples.peripheral.auth import Identity
from samples.peripheral.config import SampleSettings
from samples.peripheral.schemas import CreateWorkOrder, UpdateWorkOrder, WorkOrder
from samples.peripheral.store import PostgresStore
def test_create_and_update_record_flat_order_activity(monkeypatch):
store = PostgresStore(SampleSettings())
actor = Identity(str(uuid4()), str(uuid4()), True)
saved = {}
monkeypatch.setattr(store, "_mutate", lambda who, key, command, operation: operation(None, "normal"))
def save(_conn, _who, document):
saved.clear()
saved.update(WorkOrder.model_validate(document).model_dump(mode="json"))
return dict(saved)
monkeypatch.setattr(store, "_save_work_order", save)
monkeypatch.setattr(store, "_managed_document", lambda *_args: dict(saved))
created = store.create_work_order(actor, CreateWorkOrder(
request_id=uuid4(), title="阀门预约复核", summary="现场复核开度",
location="北区阀门", assignee="北区班组",
feedback_requirements=["回传开度照片"],
))
assert created["feedback_requirements"] == ["回传开度照片"]
assert created["activity"][0]["kind"] == "created"
assert "subtasks" not in created
updated = store.update_work_order(actor, created["work_order_id"], UpdateWorkOrder(
request_id=uuid4(), expected_revision=1, status="awaiting_reply",
progress_note="已复核开度,等待回执确认",
))
assert updated["status"] == "awaiting_reply"
assert [entry["sequence"] for entry in updated["activity"]] == [1, 2]
assert updated["activity"][1]["kind"] == "feedback"
assert updated["activity"][1]["detail"] == "已复核开度,等待回执确认"
assert "progress_note" not in updated
def test_legacy_order_reads_flat_without_claiming_unrecorded_progress():
legacy = {"work_order_id": str(uuid4()), "subtasks": [{"stages": [{"status": "done"}]}]}
projected = PostgresStore._active_managed_document(legacy)
assert "subtasks" not in projected
assert projected["feedback_requirements"] == []
assert projected["activity"] == []
+8 -25
View File
@@ -1,12 +1,10 @@
from contextlib import contextmanager
from uuid import uuid4
import pytest
from scripts.seed_peripheral_sample import (
ACTOR_ID,
previous_seed_result,
restore_seeded_resources,
restore_seeded_test_data,
)
@@ -42,8 +40,8 @@ class FakeStore:
def test_previous_seed_result_reuses_legacy_receipt():
resource_id = str(uuid4())
store = FakeStore({
"command_key": "create_work_order",
"result": {"work_order_id": resource_id},
"command_key": "create_test_data",
"result": {"record_id": resource_id},
})
seed_request_id = uuid4()
@@ -51,10 +49,10 @@ def test_previous_seed_result_reuses_legacy_receipt():
store,
"project-1",
seed_request_id=seed_request_id,
command_key="create_work_order",
command_key="create_test_data",
)
assert result == {"work_order_id": resource_id}
assert result == {"record_id": resource_id}
assert store.readonly is True
assert store.connection_value.calls[0][1] == (
"project-1",
@@ -63,29 +61,14 @@ def test_previous_seed_result_reuses_legacy_receipt():
)
def test_restore_seeded_resources_only_restores_allowed_tables():
def test_restore_seeded_test_data_clears_soft_delete_fields():
store = FakeStore()
resource_ids = [str(uuid4()), str(uuid4())]
restore_seeded_resources(
store,
"project-1",
table="work_orders",
id_column="work_order_id",
resource_ids=resource_ids,
)
restore_seeded_test_data(store, "project-1", resource_ids)
statement, parameters = store.connection_value.calls[0]
assert "UPDATE peripheral_sample.work_orders" in statement
assert "UPDATE peripheral_sample.test_data_records" in statement
assert "document=document - 'deleted_at' - 'deleted_by'" in statement
assert "deleted_at=NULL, deleted_by=NULL" in statement
assert parameters == ("project-1", resource_ids)
with pytest.raises(ValueError, match="unsupported seeded resource"):
restore_seeded_resources(
store,
"project-1",
table="requests",
id_column="request_id",
resource_ids=resource_ids,
)
+6 -13
View File
@@ -28,25 +28,18 @@ def test_build_demo_work_orders_restores_original_mock_tasks():
"running",
"awaiting_reply",
]
assert all(len(document["subtasks"]) == 3 for document in documents)
assert all(len(document["feedback_requirements"]) == 3 for document in documents)
assert all("subtasks" not in document for document in documents)
assert all("合成" not in str(document) for document in documents)
def test_demo_work_orders_keep_one_current_stage_per_active_task():
def test_demo_work_orders_show_recorded_activity_without_synthetic_stages():
documents = build_demo_work_orders()
for document in documents:
current_stages = [
stage
for subtask in document["subtasks"]
for stage in subtask["stages"]
if stage["status"] == "current"
]
assert len(current_stages) == 1
assert documents[0]["subtasks"][0]["stages"][0]["status"] == "current"
assert documents[1]["subtasks"][1]["stages"][1]["status"] == "current"
assert documents[2]["subtasks"][2]["stages"][2]["status"] == "current"
assert document["activity"]
assert all(entry["kind"] in {"created", "updated"} for entry in document["activity"])
assert "stages" not in document
def test_replace_removes_legacy_projection_before_inserting_demo_tasks():
+7 -41
View File
@@ -201,47 +201,11 @@ async def test_snapshot_projects_persisted_work_orders_and_alerts():
"source": "生产工单系统",
"dispatcher": "调度中心",
"priority": "urgent",
"subtasks": [{
"id": "inspect-valve",
"title": "阀门开度复核",
"summary": "现场复核阀门开度。",
"status": "running",
"scheduled_at": NOW.replace(hour=3),
"location": "北区阀门 V-1",
"assignee": "北区班组",
"duration_minutes": 45,
"reply_window_minutes": 20,
"stages": [
{
"id": "dispatch",
"sequence": 1,
"title": "派发",
"status": "done",
"owner": "调度中心",
"time_label": "10:20",
"description": "已派发。",
},
{
"id": "execution",
"sequence": 2,
"title": "执行中",
"status": "current",
"owner": "北区班组",
"time_label": "10:30—11:15",
"description": "现场执行中。",
},
{
"id": "reply",
"sequence": 3,
"title": "待复令",
"status": "pending",
"owner": "北区班组 → 调度中心",
"time_label": "11:15—11:35",
"description": "等待执行结果。",
},
],
"reply_requirements": ["回传阀门开度", "复核压力"],
}],
"location": "北区阀门 V-1",
"assignee": "北区班组",
"feedback_requirements": ["回传阀门开度", "复核压力"],
"activity": [{"sequence": 1, "occurred_at": NOW, "actor": "调度中心",
"kind": "created", "title": "工单创建", "detail": "等待现场复核。"}],
}
persisted_alert = {
"alert_id": "alarm-1",
@@ -282,3 +246,5 @@ async def test_snapshot_projects_persisted_work_orders_and_alerts():
"severity": "warning",
}]
assert snapshot["work_orders"][0]["code"] == "WO-20260917-001"
assert snapshot["work_orders"][0]["activity"][0]["title"] == "工单创建"
assert "subtasks" not in snapshot["work_orders"][0]