fix(client): accept exact current-only sku state
This commit is contained in:
@@ -14,8 +14,12 @@ from xml.etree import ElementTree
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import cmbuyer_client.pdd.sku_reveal_spike as reveal_module
|
||||
from cmbuyer_client.device.adb import AdbDevice, DeviceInspection
|
||||
from cmbuyer_client.pdd.sku_reveal_spike import (
|
||||
_annotate_reveal_failure,
|
||||
_RevealEvidenceAdapter,
|
||||
safe_reveal_failure_stage,
|
||||
SkuRevealSpikeCapturer,
|
||||
SkuRevealSpikeError,
|
||||
_require_safe_reveal_path,
|
||||
@@ -31,6 +35,8 @@ _FIXTURES = Path(__file__).with_name("fixtures")
|
||||
_PRODUCT = (_FIXTURES / "product_entry_8_17_0.xml").read_text(encoding="utf-8")
|
||||
_EMPTY = (_FIXTURES / "sku_panel_empty_8_17_0.xml").read_text(encoding="utf-8")
|
||||
_COLOR_ONLY = (_FIXTURES / "sku_panel_color_selected_size_hidden_8_17_0.xml").read_text(encoding="utf-8")
|
||||
_CURRENT_ONLY_EMPTY = (_FIXTURES / "sku_panel_empty_current_only_8_17_0.xml").read_text(encoding="utf-8")
|
||||
_CURRENT_ONLY_COLOR = (_FIXTURES / "sku_panel_color_selected_size_hidden_current_only_8_17_0.xml").read_text(encoding="utf-8")
|
||||
_M_SELECTED = (_FIXTURES / "sku_panel_size_m_restored_8_17_0.xml").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -171,6 +177,8 @@ def _png() -> str:
|
||||
class _FakeDevice:
|
||||
def __init__(self) -> None:
|
||||
self.hierarchy = "<hierarchy />"
|
||||
self.empty_hierarchy = _EMPTY
|
||||
self.color_hierarchy = _COLOR_ONLY
|
||||
self.after_hierarchy = _candidate_unselected()
|
||||
self.calls: list[tuple[object, ...]] = []
|
||||
self.swipe_error = False
|
||||
@@ -195,10 +203,10 @@ class _FakeDevice:
|
||||
return _png()
|
||||
if method == "click":
|
||||
if params == [865, 2218]:
|
||||
self.hierarchy = _EMPTY
|
||||
self.hierarchy = self.empty_hierarchy
|
||||
return ""
|
||||
if params == [528, 1387]:
|
||||
self.hierarchy = _COLOR_ONLY
|
||||
self.hierarchy = self.color_hierarchy
|
||||
return ""
|
||||
raise AssertionError(params)
|
||||
if method == "swipe":
|
||||
@@ -282,6 +290,50 @@ class SkuRevealSpikeTests(unittest.TestCase):
|
||||
self.assertNotIn("start", manifest)
|
||||
self.assertNotIn("end", manifest)
|
||||
|
||||
def test_current_only_empty_and_color_only_are_accepted_before_reveal(self) -> None:
|
||||
device = _FakeDevice()
|
||||
device.empty_hierarchy = _CURRENT_ONLY_EMPTY
|
||||
device.color_hierarchy = _CURRENT_ONLY_COLOR
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "evidence"
|
||||
result = self._capturer(device).capture(
|
||||
"192.168.0.173:5555",
|
||||
"937122477375",
|
||||
target,
|
||||
)
|
||||
|
||||
self.assertEqual(result.output_directory, target)
|
||||
self.assertEqual(len(_swipes(device)), 1)
|
||||
self.assertEqual(
|
||||
(target / "before" / "hierarchy.xml").read_text(encoding="utf-8"),
|
||||
_CURRENT_ONLY_COLOR,
|
||||
)
|
||||
|
||||
def test_cross_price_variant_after_color_click_is_precondition_failure_without_reveal(self) -> None:
|
||||
for empty_hierarchy, color_hierarchy in (
|
||||
(_CURRENT_ONLY_EMPTY, _COLOR_ONLY),
|
||||
(_EMPTY, _CURRENT_ONLY_COLOR),
|
||||
):
|
||||
with self.subTest():
|
||||
device = _FakeDevice()
|
||||
device.empty_hierarchy = empty_hierarchy
|
||||
device.color_hierarchy = color_hierarchy
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "evidence"
|
||||
with self.assertRaises(SkuSelectionError) as raised:
|
||||
self._capturer(device).capture(
|
||||
"192.168.0.173:5555",
|
||||
"937122477375",
|
||||
target,
|
||||
)
|
||||
self.assertEqual(
|
||||
safe_reveal_failure_stage(raised.exception),
|
||||
"reveal_precondition",
|
||||
)
|
||||
self.assertEqual(_swipes(device), [])
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
def test_ambiguous_rpc_is_read_only_reconciled_without_retry(self) -> None:
|
||||
device = _FakeDevice()
|
||||
device.swipe_error = True
|
||||
@@ -367,7 +419,7 @@ class SkuRevealSpikeTests(unittest.TestCase):
|
||||
device.jsonrpc_call = drift # type: ignore[method-assign]
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "evidence"
|
||||
with self.assertRaises(SkuSelectionError):
|
||||
with self.assertRaises(SkuSelectionError) as raised:
|
||||
self._capturer(device).capture(
|
||||
"192.168.0.173:5555",
|
||||
"937122477375",
|
||||
@@ -375,6 +427,89 @@ class SkuRevealSpikeTests(unittest.TestCase):
|
||||
)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(_swipes(device), [])
|
||||
self.assertEqual(safe_reveal_failure_stage(raised.exception), "reveal_precondition")
|
||||
|
||||
def test_reveal_failure_stages_bind_attempt_candidate_after_and_publish(self) -> None:
|
||||
device = _FakeDevice()
|
||||
|
||||
def fail_before_seal(adapter: _RevealEvidenceAdapter) -> None:
|
||||
raise SkuRevealSpikeError("private pre-seal detail")
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "pre-seal"
|
||||
with (
|
||||
patch.object(_RevealEvidenceAdapter, "reveal_size_options_once", fail_before_seal),
|
||||
self.assertRaises(SkuRevealSpikeError) as pre_seal,
|
||||
):
|
||||
self._capturer(device).capture("192.168.0.173:5555", "937122477375", target)
|
||||
self.assertEqual(safe_reveal_failure_stage(pre_seal.exception), "reveal_precondition")
|
||||
self.assertEqual(_swipes(device), [])
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
# attempted 只可能在 adapter 已封存并发送唯一手势后报告。
|
||||
device = _FakeDevice()
|
||||
original_reveal = _RevealEvidenceAdapter.reveal_size_options_once
|
||||
|
||||
def fail_after_attempt(adapter: _RevealEvidenceAdapter) -> None:
|
||||
original_reveal(adapter)
|
||||
raise SkuRevealSpikeError("private attempted detail")
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "attempted"
|
||||
with (
|
||||
patch.object(_RevealEvidenceAdapter, "reveal_size_options_once", fail_after_attempt),
|
||||
self.assertRaises(SkuRevealSpikeError) as attempted,
|
||||
):
|
||||
self._capturer(device).capture("192.168.0.173:5555", "937122477375", target)
|
||||
self.assertEqual(safe_reveal_failure_stage(attempted.exception), "reveal_attempted")
|
||||
self.assertEqual(len(_swipes(device)), 1)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
device = _FakeDevice()
|
||||
device.after_hierarchy = _COLOR_ONLY
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "candidate"
|
||||
with self.assertRaises(SkuRevealSpikeError) as candidate:
|
||||
self._capturer(device).capture("192.168.0.173:5555", "937122477375", target)
|
||||
self.assertEqual(safe_reveal_failure_stage(candidate.exception), "reveal_candidate")
|
||||
self.assertEqual(len(_swipes(device)), 1)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
device = _FakeDevice()
|
||||
original_capture = reveal_module._capture_frame
|
||||
|
||||
def fail_after_capture(adapter: object, directory: Path, hierarchy: str) -> None:
|
||||
if directory.name == "after":
|
||||
raise OSError("private after path")
|
||||
original_capture(adapter, directory, hierarchy)
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "after"
|
||||
with (
|
||||
patch.object(reveal_module, "_capture_frame", fail_after_capture),
|
||||
self.assertRaises(SkuRevealSpikeError) as after,
|
||||
):
|
||||
self._capturer(device).capture("192.168.0.173:5555", "937122477375", target)
|
||||
self.assertEqual(safe_reveal_failure_stage(after.exception), "reveal_after")
|
||||
self.assertEqual(len(_swipes(device)), 1)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
device = _FakeDevice()
|
||||
with TemporaryDirectory() as directory:
|
||||
target = Path(directory) / "publish"
|
||||
with (
|
||||
patch.object(reveal_module.os, "rename", side_effect=OSError("private publish path")),
|
||||
self.assertRaises(SkuRevealSpikeError) as publish,
|
||||
):
|
||||
self._capturer(device).capture("192.168.0.173:5555", "937122477375", target)
|
||||
self.assertEqual(safe_reveal_failure_stage(publish.exception), "reveal_publish")
|
||||
self.assertEqual(len(_swipes(device)), 1)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(list(Path(directory).glob(".*.staging-*")), [])
|
||||
|
||||
def test_complete_reveal_segment_rejects_narrow_impostor_and_invalid_bounds(self) -> None:
|
||||
for hierarchy in (
|
||||
@@ -481,12 +616,58 @@ class SkuRevealSpikeCliTests(unittest.TestCase):
|
||||
|
||||
annotated = SkuSelectionError(secret)
|
||||
_annotate_sku_entry_failure(annotated, "sku_entry_panel_verify")
|
||||
_annotate_reveal_failure(annotated, "reveal_precondition")
|
||||
self.assertIn("stage=sku_entry_panel_verify", run_with(annotated))
|
||||
|
||||
spoofed = SkuSelectionError(secret)
|
||||
setattr(spoofed, "_cmbuyer_failure_stage", "sku_entry_panel_verify")
|
||||
self.assertIn("stage=unknown", run_with(spoofed))
|
||||
|
||||
def test_cli_reports_only_formally_annotated_reveal_stage(self) -> None:
|
||||
script = _load_reveal_script()
|
||||
secret = "SERIAL=192.168.0.173:5555 <hierarchy>private</hierarchy>"
|
||||
|
||||
def run_with(error: BaseException) -> str:
|
||||
class FailingCapturer:
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
return None
|
||||
|
||||
def capture(self, *args: object, **kwargs: object) -> object:
|
||||
raise error
|
||||
|
||||
stderr = io.StringIO()
|
||||
with patch.object(script, "SkuRevealSpikeCapturer", FailingCapturer), redirect_stderr(stderr):
|
||||
self.assertEqual(
|
||||
script.main(
|
||||
[
|
||||
"--serial", "192.168.0.173:5555",
|
||||
"--goods-id", "937122477375",
|
||||
"--output-dir", "evidence",
|
||||
]
|
||||
),
|
||||
1,
|
||||
)
|
||||
output = stderr.getvalue()
|
||||
self.assertNotIn("192.168.0.173:5555", output)
|
||||
self.assertNotIn("private", output)
|
||||
return output
|
||||
|
||||
annotated = SkuRevealSpikeError(secret)
|
||||
_annotate_reveal_failure(annotated, "reveal_candidate")
|
||||
self.assertIn("stage=reveal_candidate", run_with(annotated))
|
||||
|
||||
spoofed = SkuRevealSpikeError(secret)
|
||||
setattr(spoofed, "_cmbuyer_reveal_failure_stage", "reveal_candidate")
|
||||
self.assertIn("stage=unknown", run_with(spoofed))
|
||||
|
||||
class HostileGetterError(SkuRevealSpikeError):
|
||||
def __getattribute__(self, name: str) -> object:
|
||||
if name.startswith("_cmbuyer_reveal_"):
|
||||
raise RuntimeError(secret)
|
||||
return super().__getattribute__(name)
|
||||
|
||||
self.assertIn("stage=unknown", run_with(HostileGetterError(secret)))
|
||||
|
||||
|
||||
def _load_reveal_script() -> object:
|
||||
path = Path(__file__).resolve().parents[2] / "scripts" / "capture_sku_reveal_spike.py"
|
||||
|
||||
Reference in New Issue
Block a user