feat: 安全继续失败采购核单 (#262)
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user