feat: 实现受控真实下单安全边界 (#99)

This commit is contained in:
chengma
2026-08-10 16:35:38 +08:00
parent 7106f6b024
commit e960cab1fb
30 changed files with 1562 additions and 66 deletions
+98 -3
View File
@@ -7,7 +7,11 @@ from pathlib import Path
from src.android_device_service import AndroidDeviceSearchError
from src.admin_gateway import AdminTask, ClientInfo, SubmissionReceipt
from src.mock_admin_gateway import MockAdminGateway
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
from src.pdd_purchase_adapter import (
PddLivePurchaseAdapter,
PddPurchaseAdapter,
PurchasePageState,
)
from src.task_dispatcher import TaskDispatcher, admin_task_to_new_claimed_task
from src.task_models import TaskStatus, TaskType
from src.task_repository import TaskRepository
@@ -70,12 +74,31 @@ class ReadyPurchaseAdapter(PddPurchaseAdapter):
pass
def purchase_task(task_id="PUR-001"):
class ReadyLivePurchaseAdapter(ReadyPurchaseAdapter, PddLivePurchaseAdapter):
def read_state(self):
state = super().read_state()
return PurchasePageState(
page_kind=state.page_kind,
goods_id=state.goods_id,
selected_options=state.selected_options,
quantity=state.quantity,
price_cent=state.price_cent,
candidate_count=state.candidate_count,
in_stock=True,
submit_candidate_count=1,
)
def submit_order_once(self):
self.calls.append(("purchase", "submitted_once"))
def purchase_task(task_id="PUR-001", execution_mode="dry_run"):
return AdminTask(
task_id=task_id,
task_type=TaskType.PURCHASE,
version=1,
priority=10,
execution_mode=execution_mode,
payload={
"goods_url": "https://example.test/PUR-GOODS",
"goods_id": "PUR-GOODS",
@@ -103,7 +126,14 @@ class TaskDispatcherTest(unittest.TestCase):
def tearDown(self):
self.temporary.cleanup()
def _dispatcher(self, *, purchase_ready, device_checker=lambda _serial: None):
def _dispatcher(
self,
*,
purchase_ready,
live_ready=False,
purchase_mode="dry_run",
device_checker=lambda _serial: None,
):
purchase_factory = None
if purchase_ready:
purchase_factory = (
@@ -118,6 +148,16 @@ class TaskDispatcherTest(unittest.TestCase):
lambda *_args: RecordingCollector(self.calls)
),
purchase_adapter_factory=purchase_factory,
live_purchase_adapter_factory=(
(
lambda _address, _cancelled: ReadyLivePurchaseAdapter(
self.calls
)
)
if live_ready
else None
),
purchase_mode=purchase_mode,
device_connection_checker=device_checker,
)
@@ -137,6 +177,61 @@ class TaskDispatcherTest(unittest.TestCase):
ready.claim_capabilities().purchase_mode, "dry_run"
)
def test_live_capability_requires_authorization_and_live_factory(self):
no_factory = self._dispatcher(
purchase_ready=True, purchase_mode="live"
)
authorized = self._dispatcher(
purchase_ready=True,
live_ready=True,
purchase_mode="live",
)
self.assertEqual(
no_factory.claim_capabilities().purchase_mode, "dry_run"
)
self.assertEqual(
authorized.claim_capabilities().purchase_mode, "live"
)
def test_local_live_task_never_runs_after_authorization_is_disabled(self):
task = purchase_task(execution_mode="live")
self.repository.add_claimed_task(admin_task_to_new_claimed_task(task))
with self.assertRaisesRegex(RuntimeError, "授权已关闭"):
self._dispatcher(
purchase_ready=True,
live_ready=True,
purchase_mode="dry_run",
).execute_one()
self.assertNotIn(("purchase", "submitted_once"), self.calls)
self.assertEqual(
self.repository.get_task(task.task_id).status,
TaskStatus.CLAIMED,
)
def test_live_task_is_claimed_only_by_authorized_live_dispatcher(self):
task = purchase_task(execution_mode="live")
self.gateway.enqueue_task(task, self.client.client_id)
dry_outcome = self._dispatcher(purchase_ready=True).execute_one()
live_outcome = self._dispatcher(
purchase_ready=True,
live_ready=True,
purchase_mode="live",
).execute_one()
self.assertEqual(dry_outcome.kind, "no_task")
self.assertEqual(live_outcome.kind, "manual_review")
self.assertEqual(
self.repository.get_task(task.task_id).execution_mode,
"live",
)
self.assertEqual(
self.calls.count(("purchase", "submitted_once")), 1
)
def test_claims_saves_then_dispatches_purchase_dry_run(self):
task = purchase_task()
self.gateway.enqueue_task(task, self.client.client_id)