fix(tasks): persist work-order waits without restarting runs
Generic Container CI/CD / test-build-publish (push) Successful in 4m20s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 4m20s

Dispatch receipts previously requeued active investigations and the final-only completion gate then failed waiting generations. Preserve the run, accept current interim wait reports, and wake only on actionable feedback or recovery evidence. Regression tests cover dispatch, same-generation reports, receipt revisions, cancellation and replacement ordering.
This commit is contained in:
2026-11-12 13:45:27 +08:00
parent 1892b1d1f0
commit 471ad92a47
7 changed files with 345 additions and 24 deletions
+23 -6
View File
@@ -19,36 +19,53 @@ if TYPE_CHECKING:
RECOVERY_WORKFLOWS = {"burst_report", "scada_anomaly"}
def track_work_order(task: dict, action: dict) -> None:
def track_work_order(task: dict, action: dict) -> bool:
if task.get("workflow") not in RECOVERY_WORKFLOWS or action["kind"] != "create_work_order":
return
return False
receipt = action.get("receipt") or {}
if action.get("execution_status") != "succeeded" or receipt.get("adapter") != "peripheral_sample":
return
return False
context = task.setdefault("handling_context", {})
orders = context.setdefault("work_orders", {})
order_id = receipt["receipt_id"]
if order_id not in orders:
proposed_at = action.get("proposed_at")
for prior in orders.values():
terminal_at = prior.get("terminal_recorded_at")
# Only a new decision after the failed field attempt replaces it.
# A delayed receipt for an older independent dispatch does not.
if (not prior.get("superseded_by") and prior["status"] in {"failed", "cancelled"}
and proposed_at and terminal_at
and datetime.fromisoformat(proposed_at) > datetime.fromisoformat(terminal_at)):
prior["superseded_by"] = order_id
orders[order_id] = {"status": "awaiting_completion", "action_id": str(action["action_id"])}
context.update(phase="waiting_work_order", title="等待现场工单返回")
context.pop("verification_evidence_id", None)
return True
def accept_completion(task: dict, feedback: dict) -> bool:
def accept_completion(task: dict, feedback: dict, *, recorded_at: datetime | None = None) -> bool:
context = task.get("handling_context") or {}
order_id = feedback["source_event_id"].removeprefix("peripheral_sample:")
order = context.get("work_orders", {}).get(order_id)
if order is None:
return False
order.update(status=feedback["status"], source_revision=feedback["source_revision"])
if feedback["status"] in {"failed", "cancelled"}:
if recorded_at is not None and not order.get("terminal_recorded_at"):
order["terminal_recorded_at"] = recorded_at.isoformat()
else:
order.pop("terminal_recorded_at", None)
# The synchronizer preserves the field activity's business time on ordinary
# edits. A revised field receipt can explicitly correct the verification start.
if feedback["status"] == "completed":
order["returned_at"] = feedback["occurred_at"]
else:
order.pop("returned_at", None)
orders = list(context["work_orders"].values())
if all(item["status"] == "completed" for item in orders):
if order.get("superseded_by"):
return True
orders = [item for item in context["work_orders"].values() if not item.get("superseded_by")]
if orders and all(item["status"] == "completed" for item in orders):
returned_at = max((item["returned_at"] for item in orders), key=datetime.fromisoformat)
previous = context.get("returned_at")
if (previous is None or datetime.fromisoformat(previous) != datetime.fromisoformat(returned_at)