feat: 安全继续失败采购核单 (#262)

This commit is contained in:
chengma
2026-08-18 09:55:32 +08:00
parent a92b8746b2
commit 5ea8aa3d68
8 changed files with 480 additions and 20 deletions
+112 -1
View File
@@ -4,6 +4,7 @@ import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from unittest.mock import patch
from src.admin_gateway import AdminTask, ClaimCapabilities, ClientInfo
from src.mock_admin_gateway import MockAdminGateway
@@ -19,8 +20,9 @@ from src.pdd_purchase_reconcile_adapter import (
PurchaseReconcileScan,
)
from src.purchase_task_service import PurchaseTaskService
from src.pdd_ui_event import ClaimTaskWorker
from src.task_dispatcher import TaskDispatcher, admin_task_to_new_claimed_task
from src.task_models import RunStatus, TaskStatus, TaskType
from src.task_models import RunStatus, TaskRerunBatch, TaskStatus, TaskType
from src.task_repository import TaskRepository
@@ -348,6 +350,115 @@ class PurchaseRecoveryTest(unittest.TestCase):
run.diagnostics_json["reconciliation"]["error_message"],
)
def test_manual_reconcile_retry_reuses_run_and_never_purchases(self):
task_id = "PUR-RECONCILE-RETRY"
self._interrupt_after_irreversible(task_id)
first = TaskDispatcher(
self.gateway,
self.repository,
self.client,
"USB-001",
purchase_reconcile_factory=(
lambda _address, _cancelled: ReadOnlyReconcileAdapter(
[], error=ConnectionError("首次核单失败")
)
),
device_connection_checker=lambda _serial: None,
).execute_one()
self.assertEqual(first.kind, "manual_review")
before = self.repository.latest_task_run(task_id)
assert before is not None
purchase_calls = []
reconcile_calls = []
worker = ClaimTaskWorker(
self.gateway,
self.repository,
None,
None,
live_purchase_adapter_factory=(
lambda *_args: purchase_calls.append("purchase")
),
purchase_reconcile_factory=(
lambda _address, _cancelled: ReadOnlyReconcileAdapter(
reconcile_calls
)
),
device_connection_checker=lambda _serial: None,
)
outcomes = []
worker.outcome.connect(
lambda kind, message, result_task_id: outcomes.append(
(kind, message, result_task_id)
)
)
worker._execute_rerun_batch(
TaskRerunBatch(
TaskType.PURCHASE,
(task_id,),
(task_id,),
),
self.client,
"USB-001",
)
after = self.repository.latest_task_run(task_id)
assert after is not None
self.assertEqual(outcomes[0][0], "succeeded")
self.assertEqual(purchase_calls, [])
self.assertEqual(
reconcile_calls.count(("reconcile", "737116531267")), 1
)
self.assertEqual(after.attempt_id, before.attempt_id)
self.assertEqual(after.attempt_no, before.attempt_no)
self.assertEqual(
after.irreversible_action_at, before.irreversible_action_at
)
self.assertEqual(
self.repository.get_task(task_id).status,
TaskStatus.SUCCEEDED,
)
def test_reconcile_retry_failure_stops_following_purchase(self):
worker = ClaimTaskWorker(
self.gateway,
self.repository,
None,
None,
device_connection_checker=lambda _serial: None,
)
outcomes = []
worker.outcome.connect(
lambda kind, message, task_id: outcomes.append(
(kind, message, task_id)
)
)
with (
patch.object(
worker,
"_execute_purchase_reconcile_retry",
side_effect=ValueError("核单状态已变化"),
) as reconcile,
patch.object(worker, "_execute_purchase_rerun") as purchase,
):
worker._execute_rerun_batch(
TaskRerunBatch(
TaskType.PURCHASE,
("PUR-RECONCILE", "PUR-NEW"),
("PUR-RECONCILE",),
),
self.client,
"USB-001",
)
reconcile.assert_called_once()
purchase.assert_not_called()
self.assertEqual(
[item[0] for item in outcomes], ["skipped", "skipped"]
)
self.assertIn("未开始", outcomes[1][1])
def test_no_multiple_mismatched_and_paid_candidates_need_manual_review(self):
cases = {
"EMPTY": ((), "ORDER_NOT_FOUND", False),