diff --git a/client/tests/device/test_sku_evidence_sanitizer.py b/client/tests/device/test_sku_evidence_sanitizer.py index e3ea48a..a1ba36e 100644 --- a/client/tests/device/test_sku_evidence_sanitizer.py +++ b/client/tests/device/test_sku_evidence_sanitizer.py @@ -2,7 +2,9 @@ from __future__ import annotations +from functools import lru_cache from hashlib import sha256 +from io import BytesIO import json from pathlib import Path import sys @@ -48,6 +50,16 @@ def _hash(path: Path) -> str: return digest.hexdigest() +@lru_cache(maxsize=None) +def _source_png(size: tuple[int, int]) -> bytes: + image = Image.new("RGB", size, color=(0, 180, 0)) + if size == (EXPECTED_SCREENSHOT_WIDTH, EXPECTED_SCREENSHOT_HEIGHT): + image.paste((255, 0, 0), (0, 0, 8, 540)) + output = BytesIO() + image.save(output, format="PNG") + return output.getvalue() + + def _default_xml() -> str: return ( "" @@ -116,12 +128,7 @@ def _write_raw( raw = root / "raw" raw.mkdir(parents=True) screenshot = raw / "screenshot.png" - image = Image.new("RGB", size, color=(0, 180, 0)) - if size == (EXPECTED_SCREENSHOT_WIDTH, EXPECTED_SCREENSHOT_HEIGHT): - for y in range(540): - for x in range(8): - image.putpixel((x, y), (255, 0, 0)) - image.save(screenshot, format="PNG") + screenshot.write_bytes(_source_png(size)) hierarchy = raw / "hierarchy.xml" hierarchy.write_text(_default_xml() if xml is None else xml, encoding="utf-8") manifest = { diff --git a/client/tests/pdd/test_sku_selection.py b/client/tests/pdd/test_sku_selection.py index 2b2bbad..a9f43fa 100644 --- a/client/tests/pdd/test_sku_selection.py +++ b/client/tests/pdd/test_sku_selection.py @@ -124,6 +124,31 @@ def _center(bounds: str) -> tuple[int, int]: return left + (right - left) // 2, top + (bottom - top) // 2 +def _flow(device: _RawDevice, timeout_seconds: float = 0.02) -> SkuSelectionFlow: + now = [0.0] + return SkuSelectionFlow( + UiautomatorSkuPanelAdapter(device, 10), + timeout_seconds, + 0.01, + lambda: now[0], + lambda seconds: now.__setitem__(0, now[0] + seconds), + ) + + +def _fast_runner_flow( + device: SkuPanelDevice, + entry_wait_timeout_seconds: float = 0.02, +) -> SkuSelectionFlow: + now = [0.0] + return SkuSelectionFlow( + device, + entry_wait_timeout_seconds, + 0.01, + lambda: now[0], + lambda seconds: now.__setitem__(0, now[0] + seconds), + ) + + def _mutate_unique_node( hierarchy: str, *, @@ -321,8 +346,7 @@ class SkuSelectionFlowTests(unittest.TestCase): def test_target_mapping_stops_at_unproven_reveal_then_s_to_m_is_exact(self) -> None: device = _RawDevice() - adapter = UiautomatorSkuPanelAdapter(device, 10) - flow = SkuSelectionFlow(adapter) + flow = _flow(device) flow.open_sku_panel(_TARGET_URL) with self.assertRaisesRegex(SkuSelectionError, "受控显示动作尚未取证"): @@ -332,7 +356,7 @@ class SkuSelectionFlowTests(unittest.TestCase): self.assertEqual(_actions(device, "pressKey"), []) restored = _RawDevice(_S_FIXTURE.read_text(encoding="utf-8")) - restored_flow = SkuSelectionFlow(UiautomatorSkuPanelAdapter(restored, 10)) + restored_flow = _flow(restored) restored_flow.select_sku_options(resolve_task_selection(_TASK_COLOR, _TASK_SIZE)) self.assertEqual(restored_flow.read_sku_unit_price(), "12.88") restored_flow.exit_sku_panel_safely() @@ -342,7 +366,7 @@ class SkuSelectionFlowTests(unittest.TestCase): def test_full_verified_entry_structure_taps_exact_text_child_once(self) -> None: device = _RawDevice() - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).open_sku_panel(_TARGET_URL) + _flow(device).open_sku_panel(_TARGET_URL) self.assertEqual(_tap_centers(device), [(865, 2218)]) self.assertNotIn((763, 2247), _tap_centers(device)) # 父容器中心不是获准目标。 @@ -408,7 +432,7 @@ class SkuSelectionFlowTests(unittest.TestCase): def test_unverified_home_entry_labels_do_not_trigger_old_product_rejection(self) -> None: device = _RawDevice() - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).open_sku_panel( + _flow(device).open_sku_panel( _TARGET_URL, _home_with_unverified_entry_labels(), ) @@ -477,7 +501,7 @@ class SkuSelectionFlowTests(unittest.TestCase): ): with self.subTest(): device = _RawDevice() - flow = SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)) + flow = _flow(device) with self.assertRaises(SkuSelectionError): flow.open_sku_panel(_TARGET_URL, old_page) @@ -531,7 +555,7 @@ class SkuSelectionFlowTests(unittest.TestCase): device = _RawDevice(_FIXTURE.read_text(encoding="utf-8")) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).select_sku_options( + _flow(device).select_sku_options( resolve_task_selection(_TASK_COLOR, _TASK_SIZE).__class__("粉红", "L(建议115-130)") ) self.assertEqual(_actions(device, "click"), []) @@ -546,7 +570,7 @@ class SkuSelectionFlowTests(unittest.TestCase): ) for hierarchy in cases: with self.subTest(), self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(_RawDevice(hierarchy), 10)).select_sku_options(resolve_task_selection(_TASK_COLOR, _TASK_SIZE)) + _flow(_RawDevice(hierarchy)).select_sku_options(resolve_task_selection(_TASK_COLOR, _TASK_SIZE)) def test_unknown_clickable_action_ancestor_is_rejected_by_profile(self) -> None: root = ElementTree.fromstring(_EMPTY_FIXTURE.read_text(encoding="utf-8")) @@ -585,13 +609,13 @@ class SkuSelectionFlowTests(unittest.TestCase): device = _RawDevice(_PRODUCT_PAGE.replace("[688,2184][1042,2253]", "[0,0][1081,1]")) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).open_sku_panel(_TARGET_URL) + _flow(device).open_sku_panel(_TARGET_URL) self.assertEqual(_actions(device, "click"), []) def test_color_readback_failure_never_attempts_second_option(self) -> None: device = _RawDevice() device.fail_color_readback = True - flow = SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)) + flow = _flow(device) flow.open_sku_panel(_TARGET_URL) with self.assertRaises(SkuSelectionError): flow.select_sku_options(resolve_task_selection(_TASK_COLOR, _TASK_SIZE)) @@ -599,7 +623,7 @@ class SkuSelectionFlowTests(unittest.TestCase): def test_non_target_selection_restores_each_dimension_once(self) -> None: device = _RawDevice() - flow = SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)) + flow = _flow(device) flow.open_sku_panel(_TARGET_URL) device.select_alternates() flow.select_sku_options(resolve_task_selection(_TASK_COLOR, _TASK_SIZE)) @@ -613,10 +637,10 @@ class SkuSelectionFlowTests(unittest.TestCase): with self.subTest(replacement=replacement): device = _RawDevice(_FIXTURE.read_text(encoding="utf-8").replace("快卖完 ¥12.88", replacement)) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).read_sku_unit_price() + _flow(device).read_sku_unit_price() device = _RawDevice(_FIXTURE.read_text(encoding="utf-8").replace("快卖完 ¥12.88", "提交订单 ¥12.88")) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).read_sku_unit_price() + _flow(device).read_sku_unit_price() clickable_parent = _mutate_unique_node( _FIXTURE.read_text(encoding="utf-8"), class_name="android.widget.LinearLayout", @@ -625,7 +649,7 @@ class SkuSelectionFlowTests(unittest.TestCase): value="true", ) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(_RawDevice(clickable_parent), 10)).read_sku_unit_price() + _flow(_RawDevice(clickable_parent)).read_sku_unit_price() def test_public_api_and_protocol_have_no_broad_or_order_operations(self) -> None: forbidden = {"quantity", "confirm", "authorization", "fence", "submit", "payment", "click"} @@ -671,13 +695,13 @@ class SkuSelectionFlowTests(unittest.TestCase): device = NoPanelAfterEntry() with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).open_sku_panel(_TARGET_URL) + _flow(device).open_sku_panel(_TARGET_URL) self.assertEqual(_tap_centers(device), [(865, 2218)]) duplicate = _duplicate_entry() device = _RawDevice(duplicate) with self.assertRaises(SkuSelectionError): - SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).open_sku_panel(_TARGET_URL) + _flow(device).open_sku_panel(_TARGET_URL) self.assertEqual(_actions(device, "click"), []) def test_fixture_contains_no_address_phone_or_payment_credentials(self) -> None: @@ -719,6 +743,11 @@ class _CompletedFlow: class SkuSelectionRunnerTests(unittest.TestCase): + def setUp(self) -> None: + flow_patch = patch.object(runner_module, "SkuSelectionFlow", _fast_runner_flow) + flow_patch.start() + self.addCleanup(flow_patch.stop) + def _runner(self, adb: _FakeAdb, device: _RawDevice) -> SkuSelectionRunner: device.hierarchy = "" adb.on_intent = lambda: setattr(device, "hierarchy", _PRODUCT_PAGE.replace("", '')) @@ -1018,7 +1047,7 @@ class SkuSelectionRunnerTests(unittest.TestCase): device = _RawDevice(); device.select_alternates() device.package = "other" - with self.assertRaises(SkuSelectionError): SkuSelectionFlow(UiautomatorSkuPanelAdapter(device, 10)).exit_sku_panel_safely() + with self.assertRaises(SkuSelectionError): _flow(device).exit_sku_panel_safely() self.assertEqual(_actions(device, "pressKey"), []) def test_unproven_reveal_publishes_nothing_and_safely_exits_once(self) -> None: diff --git a/docs/tasks/T-103.md b/docs/tasks/T-103.md index e24528b..f6ced91 100644 --- a/docs/tasks/T-103.md +++ b/docs/tasks/T-103.md @@ -25,7 +25,7 @@ write_paths: - docs/current-state.md --- - + ## 问题 / 背景 T-102 已证明 canonical 链接可进入目标商品。T-103 在 PKG110 / Android 16 / 拼多多 8.17.0、goods_id `937122477375` 上确认:详情页底部购买区第一行“快要抢光 + 金额”是唯一获准的受控规格入口;商品内容区同名小字和第二行“免拼购买”都不得点击。 @@ -274,6 +274,10 @@ T-103 sanitizer v2 坐标修正与主审:提交 44c027a 将 screenshot space ### 2026-08-05T04:33:56Z · ila 2026-08-05 T-103 reveal 离线实现与终审通过:新增独立 `capture_sku_reveal_spike.py`,只在拼多多 8.17.0 / PKG110 / Android 16 / 1080×2376、goods_id `937122477375`、完整 `COLOR_SELECTED_SIZE_HIDDEN` 前置两次重证后,封存并发出唯一一次固定 reveal 手势;RPC 正常或结果不明均只读调和且绝不重试。后置要求目标颜色保持精确 selected、S/M 均可见且 selected=false、摘要仍为“请选择:尺码”、提交硬拒绝区仍在面板下方;成功不 Back,等待人工核对。四态 classifier 已绑定 A/B/S/M 真机证据的完整父链与 exact selected set;未知 clickable 祖先、窄浮层、SystemUI 同 bounds、坏 bounds 均失败关闭。独立终审 PASS;聚焦 55/55、client 全量 255/255、compileall、agent-context、diff-check 全部 PASS。未引用数量、确认页、围栏、提交或支付能力。`needs_device=true`,T-103 保持 Doing,等待项目所有者从拼多多首页、仅 Wi-Fi ADB、使用全新 output-dir 执行唯一一次真机 reveal 并人工确认 before/after。 + +### 2026-08-05T04:45:40Z · ila + +2026-08-05 T-011 方案 B 由 T-103 所有者就地完成 pdd/device 测试提速:仅修改测试。`test_sku_selection` 保持 47 项,无删除/合并/skip,使用生产既有时钟注入缝和 runner 测试工厂,把 4.029s 降至 1.725s(≤2s);`test_sku_evidence_sanitizer` 保持 25 项,确认根因不是时钟而是重复编码 1080×2376 合成 PNG,改为按尺寸缓存同一确定性 PNG bytes,把 5.383s 降至 4.360s。两模块合计 72/72 PASS;与 T-304 所有者的 polling 改动合并后 client 全量仍为 255/255,12.892s(≤15s),compileall/diff-check/agent-context 均通过。未修改生产代码或任何规格/价格/提交安全断言。T-103 仍因真机 reveal 与最终完整链路人工验收保持 Doing。 ## 边界