feat: 执行并提交 PDD 采集任务 (#32)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""采集任务应用流程测试,不连接真机和网络。"""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.admin_gateway import AdminTask, ClaimCapabilities, ClientInfo
|
||||
from src.collect_task_service import CollectTaskService
|
||||
from src.mock_admin_gateway import MockAdminGateway
|
||||
from src.pdd_collect_service import PddCollectError
|
||||
from src.task_models import NewClaimedTask, TaskStatus, TaskType
|
||||
from src.task_repository import TaskRepository
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def to_pdd_data(self):
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"goods_id": "737116531267",
|
||||
"title": "测试商品",
|
||||
"shop_name": "测试店铺",
|
||||
"price_granularity": "color",
|
||||
"dimensions": [
|
||||
{"key": "color", "name": "颜色分类"},
|
||||
{"key": "size", "name": "尺码"},
|
||||
],
|
||||
"skus": [
|
||||
{
|
||||
"options": {"color": "黑色", "size": "M"},
|
||||
"price_cent": 990,
|
||||
"price_observed_at": {"color": "黑色", "size": "M"},
|
||||
"available": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class FakeCollector:
|
||||
def __init__(self, calls, error=None):
|
||||
self.calls = calls
|
||||
self.error = error
|
||||
|
||||
def collect(self, task):
|
||||
self.calls.append(task.remote_task_id)
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
return FakeResult()
|
||||
|
||||
|
||||
class CollectTaskServiceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.repository = TaskRepository(Path(self.temporary.name) / "client.db")
|
||||
self.gateway = MockAdminGateway()
|
||||
self.client = ClientInfo("CLIENT-001", "测试电脑")
|
||||
self.task = AdminTask(
|
||||
task_id="COL-001",
|
||||
task_type=TaskType.COLLECT,
|
||||
version=1,
|
||||
priority=0,
|
||||
payload={
|
||||
"goods_id": "737116531267",
|
||||
"goods_url": "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267",
|
||||
},
|
||||
created_at="2026-08-07T08:00:00Z",
|
||||
updated_at="2026-08-07T08:00:00Z",
|
||||
)
|
||||
self.gateway.enqueue_task(self.task, self.client.client_id)
|
||||
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def _service(self, calls, error=None):
|
||||
return CollectTaskService(
|
||||
self.gateway,
|
||||
self.repository,
|
||||
self.client,
|
||||
"USB-001",
|
||||
collect_service_factory=lambda *_args: FakeCollector(calls, error),
|
||||
)
|
||||
|
||||
def test_claim_collect_persist_submit_completes_one_task(self):
|
||||
calls = []
|
||||
|
||||
outcome = self._service(calls).execute_one()
|
||||
|
||||
self.assertEqual(outcome.kind, "succeeded")
|
||||
self.assertEqual(calls, ["COL-001"])
|
||||
detail = self.repository.get_task("COL-001")
|
||||
self.assertEqual(detail.status, TaskStatus.SUCCEEDED)
|
||||
self.assertEqual(detail.pdd_data["price_granularity"], "color")
|
||||
self.assertEqual(self.gateway.submission_count, 1)
|
||||
|
||||
def test_submit_timeout_retries_stored_outbox_without_recollecting(self):
|
||||
remote = self.gateway.claim_next(
|
||||
self.client,
|
||||
ClaimCapabilities(supported_types=(TaskType.COLLECT,)),
|
||||
)
|
||||
self.repository.add_claimed_task(
|
||||
NewClaimedTask(
|
||||
remote_task_id=remote.task_id,
|
||||
task_type=remote.task_type,
|
||||
goods_url=remote.payload["goods_url"],
|
||||
goods_id=remote.payload["goods_id"],
|
||||
version=remote.version,
|
||||
admin_payload={"payload": dict(remote.payload)},
|
||||
)
|
||||
)
|
||||
calls = []
|
||||
self.gateway.timeout_next_call()
|
||||
|
||||
first = self._service(calls).execute_one()
|
||||
second = self._service(calls).execute_one()
|
||||
|
||||
self.assertEqual(first.kind, "result_pending")
|
||||
self.assertEqual(second.kind, "succeeded")
|
||||
self.assertEqual(calls, ["COL-001"])
|
||||
self.assertEqual(self.gateway.submission_count, 1)
|
||||
|
||||
def test_pending_outbox_can_submit_without_android_device(self):
|
||||
remote = self.gateway.claim_next(
|
||||
self.client,
|
||||
ClaimCapabilities(supported_types=(TaskType.COLLECT,)),
|
||||
)
|
||||
self.repository.add_claimed_task(
|
||||
NewClaimedTask(
|
||||
remote_task_id=remote.task_id,
|
||||
task_type=remote.task_type,
|
||||
goods_url=remote.payload["goods_url"],
|
||||
goods_id=remote.payload["goods_id"],
|
||||
version=remote.version,
|
||||
)
|
||||
)
|
||||
started = self.repository.start_collect_run(remote.task_id, "USB-001")
|
||||
self.repository.save_collect_result(
|
||||
remote.task_id, started.attempt_id, FakeResult().to_pdd_data()
|
||||
)
|
||||
|
||||
outcome = CollectTaskService(
|
||||
self.gateway, self.repository, self.client, ""
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(outcome.kind, "succeeded")
|
||||
self.assertEqual(
|
||||
self.repository.get_task(remote.task_id).status,
|
||||
TaskStatus.SUCCEEDED,
|
||||
)
|
||||
|
||||
def test_captcha_becomes_manual_review_and_is_reported(self):
|
||||
calls = []
|
||||
outcome = self._service(
|
||||
calls, PddCollectError("PDD_PAGE_CAPTCHA", "需要验证")
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(outcome.kind, "failed")
|
||||
self.assertEqual(
|
||||
self.repository.get_task("COL-001").status,
|
||||
TaskStatus.MANUAL_REVIEW,
|
||||
)
|
||||
self.assertEqual(self.gateway.submission_count, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -104,7 +104,7 @@ class HttpAdminGatewayTest(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
)
|
||||
gateway = HttpAdminGateway(opener=opener)
|
||||
gateway = HttpAdminGateway(opener=opener, client_id="CLIENT-001")
|
||||
|
||||
gateway.register_client(
|
||||
ClientInfo("CLIENT-001"), self._capabilities(False)
|
||||
@@ -116,6 +116,71 @@ class HttpAdminGatewayTest(unittest.TestCase):
|
||||
key.lower(): value for key, value in opener.request.header_items()
|
||||
})
|
||||
|
||||
def test_submit_result_sends_idempotency_key_and_parses_receipt(self):
|
||||
opener = RecordingOpener(
|
||||
FakeResponse(
|
||||
200,
|
||||
{
|
||||
"accepted": True,
|
||||
"result_id": "RESULT-001",
|
||||
"accepted_at": "2026-08-07T08:00:01Z",
|
||||
},
|
||||
)
|
||||
)
|
||||
gateway = HttpAdminGateway(opener=opener, client_id="CLIENT-001")
|
||||
payload = {
|
||||
"task_version": 1,
|
||||
"attempt_id": "ATTEMPT-001",
|
||||
"result_type": "collect",
|
||||
"completed_at": "2026-08-07T08:00:00Z",
|
||||
"pdd_data": {"goods_id": "123"},
|
||||
}
|
||||
|
||||
receipt = gateway.submit_result(
|
||||
"COL-001", "COL-001:ATTEMPT-001:result-v1", payload
|
||||
)
|
||||
|
||||
self.assertTrue(receipt.accepted)
|
||||
self.assertEqual(
|
||||
opener.request.full_url,
|
||||
"http://127.0.0.1:8080/api/v1/client/tasks/COL-001/result",
|
||||
)
|
||||
headers = {
|
||||
key.lower(): value for key, value in opener.request.header_items()
|
||||
}
|
||||
self.assertEqual(
|
||||
headers["idempotency-key"], "COL-001:ATTEMPT-001:result-v1"
|
||||
)
|
||||
self.assertEqual(headers["x-client-id"], "CLIENT-001")
|
||||
self.assertEqual(json.loads(opener.request.data), payload)
|
||||
|
||||
def test_submit_failure_uses_failure_endpoint(self):
|
||||
opener = RecordingOpener(
|
||||
FakeResponse(
|
||||
201,
|
||||
{
|
||||
"accepted": True,
|
||||
"result_id": "FAILURE-001",
|
||||
"accepted_at": "2026-08-07T08:00:01Z",
|
||||
},
|
||||
)
|
||||
)
|
||||
gateway = HttpAdminGateway(opener=opener, client_id="CLIENT-001")
|
||||
|
||||
gateway.submit_failure(
|
||||
"COL-001",
|
||||
"COL-001:ATTEMPT-001:failure-v1",
|
||||
{
|
||||
"task_version": 1,
|
||||
"attempt_id": "ATTEMPT-001",
|
||||
"status": "manual_review",
|
||||
"error": {"code": "PDD_PAGE_CAPTCHA"},
|
||||
"reported_at": "2026-08-07T08:00:00Z",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(opener.request.full_url.endswith("/COL-001/failure"))
|
||||
|
||||
def test_claim_maps_real_admin_payload_and_only_reports_collect(self):
|
||||
opener = RecordingOpener(
|
||||
FakeResponse(
|
||||
|
||||
@@ -62,6 +62,18 @@ class LoadingDevice(FakeCollectDevice):
|
||||
return '<hierarchy><node text="正在加载" /></hierarchy>'
|
||||
|
||||
|
||||
class DisconnectingDevice(FakeCollectDevice):
|
||||
def __init__(self, home_xml, spec_xml):
|
||||
super().__init__(home_xml, spec_xml)
|
||||
self.current_calls = 0
|
||||
|
||||
def app_current(self):
|
||||
self.current_calls += 1
|
||||
if self.current_calls > 1:
|
||||
raise RuntimeError("device not found")
|
||||
return super().app_current()
|
||||
|
||||
|
||||
def keep_only_one_sku(xml_data: str) -> str:
|
||||
"""从脱敏固件中删除蓝色和 L,只保留一个组合。"""
|
||||
|
||||
@@ -114,6 +126,19 @@ class PddCollectParserTest(unittest.TestCase):
|
||||
["M", "L"],
|
||||
)
|
||||
|
||||
def test_truncated_spec_name_prefers_full_content_description(self):
|
||||
xml_data = self.spec_xml.replace(
|
||||
'content-desc="红色" clickable="true"',
|
||||
'text="红…" content-desc="红色" clickable="true"',
|
||||
)
|
||||
result = parse_spec_panel(xml_data)
|
||||
self.assertEqual(result.dimensions[0].values[0].text, "红色")
|
||||
|
||||
without_description = xml_data.replace(' content-desc="红色"', "")
|
||||
with self.assertRaises(PddCollectError) as raised:
|
||||
parse_spec_panel(without_description)
|
||||
self.assertEqual(raised.exception.code, "PDD_DATA_SKU_NAME_TRUNCATED")
|
||||
|
||||
def test_login_and_invalid_xml_have_different_error_codes(self):
|
||||
with self.assertRaises(PddCollectError) as login:
|
||||
parse_goods_page('<hierarchy><node text="手机号登录" /></hierarchy>')
|
||||
@@ -150,14 +175,46 @@ class PddCollectParserTest(unittest.TestCase):
|
||||
data = result.to_pdd_data()
|
||||
|
||||
self.assertEqual(data["schema_version"], 1)
|
||||
self.assertEqual(data["goods"]["goods_id"], "123")
|
||||
self.assertEqual(data["goods_id"], "123")
|
||||
self.assertEqual(data["title"], "测试纯棉短袖商品")
|
||||
self.assertEqual(data["shop_name"], "测试服饰旗舰店")
|
||||
self.assertEqual(data["price_granularity"], "color")
|
||||
self.assertEqual(data["metrics"]["sales"]["value"], 12000)
|
||||
self.assertEqual(data["dimensions"][0]["key"], "color")
|
||||
self.assertEqual(data["skus"][0]["options"], {"color": "红色", "size": "M"})
|
||||
self.assertEqual(data["skus"][0]["price_cent"], 1000)
|
||||
self.assertEqual(
|
||||
data["skus"][0]["price_observed_at"],
|
||||
{"color": "红色", "size": "M"},
|
||||
)
|
||||
self.assertEqual(data["source"]["device_address"], "USB-001")
|
||||
self.assertIsNone(data["purchase"])
|
||||
|
||||
def test_price_is_sampled_once_per_available_color(self):
|
||||
device = FakeCollectDevice(self.home_xml, self.spec_xml)
|
||||
service = PddCollectService(
|
||||
PddDeviceService(lambda _serial: device),
|
||||
"USB-001",
|
||||
"client-001",
|
||||
sleeper=lambda _seconds: None,
|
||||
max_page_swipes=0,
|
||||
max_spec_swipes=0,
|
||||
)
|
||||
|
||||
data = service.collect(
|
||||
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
|
||||
).to_pdd_data()
|
||||
|
||||
self.assertEqual(len(data["skus"]), 4)
|
||||
red_skus = [
|
||||
item for item in data["skus"] if item["options"]["color"] == "红色"
|
||||
]
|
||||
self.assertEqual(
|
||||
{item["price_observed_at"]["size"] for item in red_skus}, {"M"}
|
||||
)
|
||||
# 1 次打开规格面板,另有红色点 1 次;尺码只读不点击。
|
||||
self.assertEqual(len(device.clicks), 2)
|
||||
|
||||
def test_missing_goods_id_is_structured_error(self):
|
||||
service = PddCollectService(
|
||||
PddDeviceService(lambda _serial: object()),
|
||||
@@ -195,7 +252,7 @@ class PddCollectParserTest(unittest.TestCase):
|
||||
|
||||
def test_page_timeout_has_specific_error_code(self):
|
||||
device = LoadingDevice(self.home_xml, self.spec_xml)
|
||||
ticks = iter((0.0, 2.0))
|
||||
ticks = iter((0.0, 0.0, 0.0, 2.0))
|
||||
service = PddCollectService(
|
||||
PddDeviceService(lambda _serial: device),
|
||||
"USB-001",
|
||||
@@ -211,7 +268,21 @@ class PddCollectParserTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "PDD_PAGE_TIMEOUT")
|
||||
|
||||
def test_incomplete_goods_data_does_not_return_partial_success(self):
|
||||
def test_runtime_device_disconnect_has_stable_error_code(self):
|
||||
service = PddCollectService(
|
||||
PddDeviceService(
|
||||
lambda _serial: DisconnectingDevice(self.home_xml, self.spec_xml)
|
||||
),
|
||||
"USB-001",
|
||||
"client-001",
|
||||
)
|
||||
with self.assertRaises(PddCollectError) as raised:
|
||||
service.collect(
|
||||
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "DEVICE_DISCONNECTED")
|
||||
|
||||
def test_missing_shop_name_is_allowed(self):
|
||||
incomplete_home = self.home_xml.replace(
|
||||
'<node class="android.widget.TextView" text="店铺:测试服饰旗舰店" '
|
||||
'bounds="[30,1050][600,1110]" visible-to-user="true" '
|
||||
@@ -227,11 +298,10 @@ class PddCollectParserTest(unittest.TestCase):
|
||||
max_page_swipes=0,
|
||||
)
|
||||
|
||||
with self.assertRaises(PddCollectError) as raised:
|
||||
service.collect(
|
||||
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "PDD_DATA_SHOP_MISSING")
|
||||
result = service.collect(
|
||||
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
|
||||
)
|
||||
self.assertIsNone(result.shop_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""用真机导出的控件树验证真实页面结构。"""
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from src.pdd_collect_service import parse_goods_page, parse_spec_panel
|
||||
|
||||
|
||||
REAL_XML = Path(__file__).parents[1] / "image_xml"
|
||||
|
||||
|
||||
class PddRealXmlFixtureTest(unittest.TestCase):
|
||||
def test_split_title_is_joined_from_real_goods_page(self):
|
||||
result = parse_goods_page(
|
||||
(REAL_XML / "737116531267_home.xml").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
result.title,
|
||||
"【店铺热销】冰丝吊带裙女夏季薄款内搭连衣裙宽松a字裙中长款打底裙衬裙纯色",
|
||||
)
|
||||
|
||||
def test_real_spec_panel_has_all_colors_and_two_prices(self):
|
||||
result = parse_spec_panel(
|
||||
(REAL_XML / "737116531267_size.xml").read_text(encoding="utf-8")
|
||||
)
|
||||
colors = next(item for item in result.dimensions if item.key == "color")
|
||||
|
||||
self.assertEqual(len(colors.values), 8)
|
||||
self.assertIn("浅蓝色中长款", [item.text for item in colors.values])
|
||||
self.assertEqual(result.price_cent, 470)
|
||||
self.assertEqual(result.list_price_cent, 1990)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,7 +12,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from src.pdd_ui import PDDTaskPage
|
||||
from src.admin_gateway import AdminTask
|
||||
from src.admin_gateway import AdminTask, SubmissionReceipt
|
||||
from src.pdd_ui_event import (
|
||||
PDDTaskPageEvent,
|
||||
admin_task_to_new_claimed_task,
|
||||
@@ -38,6 +38,12 @@ class BrokenSaveRepository(BrokenRepository):
|
||||
def add_claimed_task(self, _task):
|
||||
raise RuntimeError("disk is full")
|
||||
|
||||
def next_pending_outbox(self):
|
||||
return None
|
||||
|
||||
def next_collect_task(self):
|
||||
return None
|
||||
|
||||
|
||||
class RecordingClaimGateway:
|
||||
"""记录领取参数并返回预设结果。"""
|
||||
@@ -52,6 +58,12 @@ class RecordingClaimGateway:
|
||||
self.thread_ids.append(threading.get_ident())
|
||||
return self.response
|
||||
|
||||
def submit_result(self, task_id, idempotency_key, result):
|
||||
return SubmissionReceipt(True, "RESULT-001", "2026-08-07T08:00:00Z")
|
||||
|
||||
def submit_failure(self, task_id, idempotency_key, failure):
|
||||
return SubmissionReceipt(True, "FAILURE-001", "2026-08-07T08:00:00Z")
|
||||
|
||||
|
||||
class SlowClaimGateway(RecordingClaimGateway):
|
||||
"""让关闭测试能稳定发生在 HTTP 返回之前。"""
|
||||
@@ -66,6 +78,28 @@ class SlowClaimGateway(RecordingClaimGateway):
|
||||
return super().claim_next(client, capabilities)
|
||||
|
||||
|
||||
class FakeCollectResult:
|
||||
def to_pdd_data(self):
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"goods_id": "737116531267",
|
||||
"title": "测试商品",
|
||||
"shop_name": "测试店铺",
|
||||
"price_granularity": "color",
|
||||
"dimensions": [],
|
||||
"skus": [{"price_cent": 990}],
|
||||
}
|
||||
|
||||
|
||||
class FakeCollector:
|
||||
def collect(self, _task):
|
||||
return FakeCollectResult()
|
||||
|
||||
|
||||
def fake_collect_factory(*_args):
|
||||
return FakeCollector()
|
||||
|
||||
|
||||
def collect_admin_task(task_id="COL-001"):
|
||||
return AdminTask(
|
||||
task_id=task_id,
|
||||
@@ -246,6 +280,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
@@ -276,6 +311,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
@@ -295,6 +331,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
@@ -316,12 +353,14 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertIn("本地已有", page.statusLabel.text())
|
||||
self.assertIn("采集完成", page.statusLabel.text())
|
||||
self.assertEqual(gateway.calls, [])
|
||||
self.assertEqual(self.repository.count_tasks(), 1)
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
@@ -334,6 +373,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
BrokenSaveRepository(),
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
@@ -371,6 +411,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
)
|
||||
page.autoFetchRequested.emit()
|
||||
self.assertTrue(wait_until(self.app, gateway.started.is_set))
|
||||
|
||||
@@ -183,6 +183,53 @@ class TaskRepositoryTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "offset"):
|
||||
self.repository.list_tasks(offset=-1)
|
||||
|
||||
def test_result_and_outbox_are_saved_together_then_marked_succeeded(self):
|
||||
self.repository.add_claimed_task(self._task("TASK-RUN"))
|
||||
started = self.repository.start_collect_run("TASK-RUN", "USB-001")
|
||||
event = self.repository.save_collect_result(
|
||||
"TASK-RUN",
|
||||
started.attempt_id,
|
||||
{
|
||||
"goods_id": "10001",
|
||||
"title": "测试商品",
|
||||
"price_granularity": "color",
|
||||
"dimensions": [],
|
||||
"skus": [{"price_cent": 990}],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.repository.get_task("TASK-RUN").status,
|
||||
TaskStatus.RESULT_PENDING,
|
||||
)
|
||||
self.assertEqual(event.status, OutboxStatus.PENDING)
|
||||
self.repository.mark_outbox_sent(event.id)
|
||||
self.assertEqual(
|
||||
self.repository.get_task("TASK-RUN").status,
|
||||
TaskStatus.SUCCEEDED,
|
||||
)
|
||||
|
||||
def test_recovery_restores_sending_and_interrupted_running(self):
|
||||
self.repository.add_claimed_task(self._task("TASK-RECOVER"))
|
||||
started = self.repository.start_collect_run("TASK-RECOVER", "USB-001")
|
||||
self.repository.save_collect_failure(
|
||||
"TASK-RECOVER",
|
||||
started.attempt_id,
|
||||
TaskStatus.RETRY_WAIT,
|
||||
"DEVICE_OFFLINE",
|
||||
"设备离线",
|
||||
True,
|
||||
)
|
||||
event = self.repository.next_pending_outbox()
|
||||
self.repository.mark_outbox_sending(event.id)
|
||||
|
||||
self.repository.recover_interrupted_work()
|
||||
|
||||
self.assertEqual(
|
||||
self.repository.get_outbox_event(event.id).status,
|
||||
OutboxStatus.PENDING,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user