fix: 修复 PDD 页面误判与失败回执 (#35)

This commit is contained in:
chengma
2026-08-07 17:58:30 +08:00
parent 51c2292be1
commit c59b4b0d59
11 changed files with 267 additions and 11 deletions
+37
View File
@@ -181,6 +181,43 @@ class HttpAdminGatewayTest(unittest.TestCase):
self.assertTrue(opener.request.full_url.endswith("/COL-001/failure"))
def test_submit_failure_accepts_legacy_admin_receipt_without_result_id(self):
gateway = HttpAdminGateway(
opener=RecordingOpener(
FakeResponse(
200,
{
"accepted": True,
"task_status": "assigned",
"accepted_at": "2026-08-07T08:00:01Z",
},
)
),
client_id="CLIENT-001",
)
receipt = gateway.submit_failure(
"COL-001",
"COL-001:ATTEMPT-001:failure-v1",
{"status": "retry_wait"},
)
self.assertTrue(receipt.accepted)
self.assertTrue(receipt.result_id.startswith("legacy-failure-"))
def test_malformed_2xx_submission_is_retryable_and_not_called_rejected(self):
gateway = HttpAdminGateway(
opener=RecordingOpener(FakeResponse(200, {"accepted": True})),
client_id="CLIENT-001",
)
with self.assertRaises(AdminGatewayError) as raised:
gateway.submit_result("COL-001", "key-1", {})
self.assertEqual(raised.exception.code, "ADMIN_INVALID_RESPONSE")
self.assertTrue(raised.exception.retryable)
self.assertIn("可能已接收", str(raised.exception))
def test_claim_maps_real_admin_payload_and_only_reports_collect(self):
opener = RecordingOpener(
FakeResponse(
+80
View File
@@ -3,6 +3,7 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
import tempfile
import unittest
import xml.etree.ElementTree as ET
@@ -74,6 +75,16 @@ class DisconnectingDevice(FakeCollectDevice):
return super().app_current()
class FocusMismatchDevice(FakeCollectDevice):
"""系统焦点报告为设置页,但无障碍树实际属于 PDD。"""
def app_current(self):
return {"package": "com.oplus.wirelesssettings"}
def app_start(self, _package):
self.app_started = True
def keep_only_one_sku(xml_data: str) -> str:
"""从脱敏固件中删除蓝色和 L,只保留一个组合。"""
@@ -190,6 +201,29 @@ class PddCollectParserTest(unittest.TestCase):
self.assertEqual(data["source"]["device_address"], "USB-001")
self.assertIsNone(data["purchase"])
def test_pdd_hierarchy_is_ready_even_when_focused_package_is_settings(self):
pdd_home_xml = self.home_xml.replace(
"<node ", '<node package="com.xunmeng.pinduoduo" '
)
device = FocusMismatchDevice(
pdd_home_xml, keep_only_one_sku(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,
)
result = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(result.goods_id, "123")
self.assertTrue(device.app_started)
def test_price_is_sampled_once_per_available_color(self):
device = FakeCollectDevice(self.home_xml, self.spec_xml)
service = PddCollectService(
@@ -268,6 +302,52 @@ class PddCollectParserTest(unittest.TestCase):
)
self.assertEqual(raised.exception.code, "PDD_PAGE_TIMEOUT")
def test_non_pdd_tree_with_purchase_words_is_not_ready(self):
device = FocusMismatchDevice(
'<hierarchy><node package="com.android.settings" text="免拼购买" '
'bounds="[0,0][100,100]" /></hierarchy>',
self.spec_xml,
)
ticks = iter((0.0, 0.0, 0.0, 2.0))
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
monotonic=lambda: next(ticks),
page_timeout=1.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_PAGE_TIMEOUT")
def test_page_timeout_saves_last_xml_diagnostic(self):
with tempfile.TemporaryDirectory() as directory:
device = LoadingDevice(self.home_xml, self.spec_xml)
ticks = iter((0.0, 0.0, 0.0, 0.0, 0.0, 2.0))
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
monotonic=lambda: next(ticks),
page_timeout=1.0,
artifact_directory=Path(directory),
)
with self.assertRaises(PddCollectError) as raised:
service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
artifacts = raised.exception.diagnostics["artifacts"]
self.assertEqual(len(artifacts), 1)
self.assertTrue(Path(artifacts[0]["path"]).is_file())
self.assertEqual(len(artifacts[0]["sha256"]), 64)
def test_runtime_device_disconnect_has_stable_error_code(self):
service = PddCollectService(
PddDeviceService(
+45 -1
View File
@@ -3,13 +3,57 @@
from pathlib import Path
import unittest
from src.pdd_collect_service import parse_goods_page, parse_spec_panel
from src.pdd_collect_service import (
PddCollectService,
parse_goods_page,
parse_spec_panel,
)
from src.pdd_device_service import PddDeviceService
REAL_XML = Path(__file__).parents[1] / "image_xml"
class SettingsFocusedRealPage:
"""复现真机:焦点 API 是无线设置,控件树实际是 PDD。"""
def __init__(self, xml_data):
self.xml_data = xml_data
def app_current(self):
return {"package": "com.oplus.wirelesssettings"}
def app_start(self, _package):
return None
def app_wait(self, _package, timeout=10):
return 1
def open_url(self, _url):
return None
def dump_hierarchy(self):
return self.xml_data
class PddRealXmlFixtureTest(unittest.TestCase):
def test_real_pdd_page_is_ready_when_focus_api_reports_settings(self):
xml_data = (REAL_XML / "737116531267_home.xml").read_text(
encoding="utf-8"
)
device = SettingsFocusedRealPage(xml_data)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
)
service._open_goods(
device,
"https://mobile.yangkeduo.com/goods.html?goods_id=737116531267",
)
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")
+20
View File
@@ -230,6 +230,26 @@ class TaskRepositoryTests(unittest.TestCase):
OutboxStatus.PENDING,
)
def test_recovery_retries_old_ambiguous_admin_response(self):
self.repository.add_claimed_task(self._task("TASK-AMBIGUOUS"))
started = self.repository.start_collect_run("TASK-AMBIGUOUS", "USB-001")
event = self.repository.save_collect_failure(
"TASK-AMBIGUOUS",
started.attempt_id,
TaskStatus.RETRY_WAIT,
"PDD_PAGE_TIMEOUT",
"页面超时",
True,
)
self.repository.mark_outbox_failed(event.id, "Admin 提交响应字段不完整")
self.repository.recover_interrupted_work()
self.assertEqual(
self.repository.get_outbox_event(event.id).status,
OutboxStatus.PENDING,
)
if __name__ == "__main__":
unittest.main()