"""只读订单核对 XML 解析测试;不连接真实手机。""" import unittest from contextlib import AbstractContextManager from src.pdd_purchase_reconcile_adapter import ( PurchaseOrderCandidate, PurchaseReconcileQuery, ) from src.pdd_u2_purchase_reconcile_adapter import ( PddPurchaseReconcileError, U2PddPurchaseReconcileAdapter, _is_single_order_detail, _merge_order_evidence, _parse_xml, parse_order_candidates, ) from src.purchase_reconcile_service import PurchaseReconcileService def query() -> PurchaseReconcileQuery: return PurchaseReconcileQuery( goods_id="737116531267", options={"color": "黑色", "size": "L"}, quantity=2, unit_price_cent=4200, total_price_cent=8400, irreversible_action_at="2026-08-10T08:00:00Z", order_submitted_at="2026-08-10T08:03:00Z", reconcile_started_at="2026-08-10T08:05:00Z", ) def xml_card(text: str) -> str: return ( '' '' f'' "" ) def xml_detail(text: str) -> str: package = "com.xunmeng.pinduoduo" return ( '' '' f'' f'' f'' "" ) class FakeSession(AbstractContextManager): def __init__(self, device): self.device = device def __enter__(self): return self.device def __exit__(self, exc_type, exc_value, traceback): return None class FakeDeviceService: def __init__(self, device): self.device = device def connect(self, device_address): return FakeSession(self.device) class DetailDevice: def __init__(self, pages): self.pages = pages self.page_index = 0 self.app_current_calls = 0 self.app_start_calls = 0 self.swipes = 0 def dump_hierarchy(self, compressed=False): return self.pages[self.page_index] def app_current(self): self.app_current_calls += 1 return {"package": "com.android.settings"} def app_start(self, package): self.app_start_calls += 1 def window_size(self): return 1080, 2400 def swipe(self, *args): self.swipes += 1 self.page_index = min(self.page_index + 1, len(self.pages) - 1) class FakeClock: def __init__(self): self.now = 0.0 def monotonic(self): return self.now def sleep(self, seconds): self.now += seconds def xml_package_page(package: str, text: str = "") -> str: return ( '' f'' f'' f'' "" ) class TransitionDevice(DetailDevice): def __init__(self, pages, current_package="com.tencent.mm"): super().__init__(pages) self.current_package = current_package self.dump_calls = 0 self.back_calls = 0 def dump_hierarchy(self, compressed=False): index = min(self.dump_calls, len(self.pages) - 1) self.dump_calls += 1 return self.pages[index] def app_current(self): self.app_current_calls += 1 return {"package": self.current_package} def press(self, key): if key == "back": self.back_calls += 1 def swipe(self, *args): self.swipes += 1 class PddU2PurchaseReconcileAdapterTest(unittest.TestCase): def test_parser_extracts_only_required_unpaid_order_fields(self): candidates = parse_order_candidates( xml_card( "待付款 订单编号:ORDER-20260810 商品编号:737116531267 " "黑色 L 共2件 合计 ¥84.00 下单时间:2026-08-10 16:03:00" ), query(), ) self.assertEqual(len(candidates), 1) candidate = candidates[0] self.assertEqual(candidate.order_no, "ORDER-20260810") self.assertEqual(candidate.goods_id, "737116531267") self.assertEqual(candidate.options, {"color": "黑色", "size": "L"}) self.assertEqual(candidate.quantity, 2) self.assertEqual(candidate.total_price_cent, 8400) self.assertEqual(candidate.ordered_at, "2026-08-10T08:03:00Z") self.assertEqual(candidate.payment_status, "unpaid") self.assertFalse(hasattr(candidate, "recipient")) self.assertFalse(hasattr(candidate, "address")) def test_paid_or_cancelled_text_never_becomes_unpaid(self): candidates = parse_order_candidates( xml_card( "待付款 已付款 订单编号:ORDER-PAID 商品编号:737116531267 " "黑色 L 共2件 合计 ¥84.00 下单时间:2026-08-10 16:03:00" ), query(), ) self.assertEqual(candidates[0].payment_status, "other") def test_missing_goods_id_and_time_are_kept_incomplete_for_manual_review(self): candidates = parse_order_candidates( xml_card("待付款 订单编号:ORDER-INCOMPLETE 黑色 L 共2件 合计 ¥84.00"), query(), ) self.assertEqual(candidates[0].goods_id, "") self.assertEqual(candidates[0].ordered_at, "") def test_adapter_merges_one_order_detail_across_viewports(self): device = DetailDevice( [ xml_detail( "待付款 商品编号:737116531267 黑色 L " "共2件 合计 ¥84.00" ), xml_detail( "订单编号:ORDER-20260810 " "下单时间:2026-08-10 16:03:00" ), ] ) adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), settle_seconds=0, max_pages=3, ) scan = adapter.read_order_candidates(query()) adapter.close() self.assertEqual(len(scan.candidates), 1) candidate = scan.candidates[0] self.assertEqual(candidate.order_no, "ORDER-20260810") self.assertEqual(candidate.goods_id, "737116531267") self.assertEqual(candidate.options, {"color": "黑色", "size": "L"}) self.assertEqual(candidate.quantity, 2) self.assertEqual(candidate.total_price_cent, 8400) self.assertEqual(candidate.ordered_at, "2026-08-10T08:03:00Z") self.assertEqual(candidate.payment_status, "unpaid") self.assertTrue(PurchaseReconcileService._candidate_matches(candidate, query())) self.assertEqual(device.app_current_calls, 0) self.assertEqual(device.app_start_calls, 0) def test_conflicting_order_numbers_are_not_merged(self): first = PurchaseOrderCandidate( order_no="ORDER-A", goods_id="737116531267", payment_status="unpaid", ) second = PurchaseOrderCandidate( order_no="ORDER-B", quantity=2, total_price_cent=8400, payment_status="unpaid", ) self.assertIsNone(_merge_order_evidence(first, second)) def test_minimal_order_and_time_candidate_uses_five_minute_window(self): within_window = PurchaseOrderCandidate( order_no="ORDER-MINIMAL", ordered_at="2026-08-10T08:08:00Z", payment_status="unpaid", ) outside_window = PurchaseOrderCandidate( order_no="ORDER-LATE", ordered_at="2026-08-10T08:08:01Z", payment_status="unpaid", ) self.assertTrue( PurchaseReconcileService._candidate_matches( within_window, query() ) ) self.assertFalse( PurchaseReconcileService._candidate_matches( outside_window, query() ) ) def test_multiple_order_actions_disable_detail_evidence_merge(self): root = _parse_xml( '' '' '' '' "" ) self.assertFalse(_is_single_order_detail(root)) def test_waits_for_pdd_detail_after_external_transition(self): complete_detail = xml_detail( "待付款 订单编号:ORDER-20260810 " "商品编号:737116531267 黑色 L 共2件 合计 ¥84.00 " "下单时间:2026-08-10 16:03:00" ) device = TransitionDevice( [ xml_package_page("com.tencent.mm", "微信登录"), xml_package_page("com.xunmeng.pinduoduo", "页面加载中"), complete_detail, ] ) clock = FakeClock() adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), settle_seconds=1, transition_timeout_seconds=30, max_pages=2, monotonic=clock.monotonic, sleep=clock.sleep, ) scan = adapter.read_order_candidates(query()) adapter.close() self.assertEqual(len(scan.candidates), 1) self.assertEqual(device.app_start_calls, 1) self.assertEqual(device.app_current_calls, 0) self.assertLess(clock.now, 30) def test_payment_back_is_pressed_at_most_once_while_page_changes(self): payment_page = xml_package_page( "com.xunmeng.pinduoduo", "立即支付" ) device = TransitionDevice( [payment_page, payment_page, xml_detail("待付款")], current_package="com.xunmeng.pinduoduo", ) clock = FakeClock() adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), settle_seconds=1, transition_timeout_seconds=30, monotonic=clock.monotonic, sleep=clock.sleep, ) adapter._open_unpaid_orders(device) self.assertEqual(device.back_calls, 1) def test_transition_timeout_reports_last_waiting_reason(self): device = TransitionDevice( [xml_package_page("com.xunmeng.pinduoduo", "页面加载中")], current_package="com.xunmeng.pinduoduo", ) clock = FakeClock() adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), settle_seconds=1, transition_timeout_seconds=2, monotonic=clock.monotonic, sleep=clock.sleep, ) with self.assertRaises(PddPurchaseReconcileError) as captured: adapter._open_unpaid_orders(device) self.assertEqual(captured.exception.code, "RECONCILE_ORDER_PAGE_TIMEOUT") self.assertIn("页面仍在过渡", str(captured.exception)) def test_transition_wait_can_be_cancelled(self): device = TransitionDevice( [xml_package_page("com.xunmeng.pinduoduo", "页面加载中")], current_package="com.xunmeng.pinduoduo", ) clock = FakeClock() checks = iter((False, False, True)) adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), cancelled=lambda: next(checks, True), settle_seconds=1, transition_timeout_seconds=30, monotonic=clock.monotonic, sleep=clock.sleep, ) with self.assertRaises(PddPurchaseReconcileError) as captured: adapter.read_order_candidates(query()) self.assertEqual(captured.exception.code, "RECONCILE_CANCELLED") def test_security_pages_still_stop_transition_wait(self): cases = ( ("请完成验证", "RECONCILE_CAPTCHA"), ("手机号登录", "RECONCILE_LOGIN_REQUIRED"), ("操作频繁", "RECONCILE_RISK_CONTROL"), ) for marker, error_code in cases: with self.subTest(marker=marker): device = TransitionDevice( [xml_package_page("com.xunmeng.pinduoduo", marker)], current_package="com.xunmeng.pinduoduo", ) clock = FakeClock() adapter = U2PddPurchaseReconcileAdapter( "serial", device_service=FakeDeviceService(device), settle_seconds=1, transition_timeout_seconds=30, monotonic=clock.monotonic, sleep=clock.sleep, ) with self.assertRaises(PddPurchaseReconcileError) as captured: adapter._open_unpaid_orders(device) self.assertEqual(captured.exception.code, error_code) self.assertEqual(device.app_start_calls, 0) self.assertEqual(device.back_calls, 0) if __name__ == "__main__": unittest.main()