feat(trading): 下单流程每步证据带整页截图(html + png + meta 三件套)

- site_interact 新增 PageSnapshot{html, screenshot} 证据载体:
  add_to_cart 截商品页、verify_cart 截 cart 页、enter_checkout 截落地
  确认页、pay 截付款检查后的完成页;截图全部 best-effort,失败记
  warning 留空,不掩盖动作结果
- submit_order 改返回 SubmitOutcome{site_order_id, evidence}:完成页
  HTML+截图首次随 step 4 落盘(此前只有 meta)
- fetch_order_detail 详情页截图挂在 OrderStatusSnapshot.screenshot,
  监控步骤 write_step 带 png;只读查询通道按字段挑出,不受影响
- runner._run_step 认 PageSnapshot(旧 str 契约保留兼容),step 0/3/4
  显式传 png
- 测试:桩同步新契约;新增每步三件套齐全的全流程断言;clear_cart
  场景单测补截图断言
- 真账号复验 verify_cart_clear.py:两场景截图均合法 PNG,落盘
  .probe/cart_clear/ 并目检为空车页渲染
This commit is contained in:
2026-08-17 08:56:11 +08:00
parent 086ab82614
commit f31e127ba7
5 changed files with 304 additions and 58 deletions
+21 -5
View File
@@ -535,6 +535,9 @@ class _FakeOrderPage:
async def content(self) -> str:
return self._html
async def screenshot(self, *, full_page: bool = True) -> bytes:
return b"fake-png"
async def close(self) -> None:
self.closed = True
@@ -1283,6 +1286,9 @@ class _FakeClearCartPage:
async def content(self) -> str:
return self._html
async def screenshot(self, *, full_page: bool = True) -> bytes:
return b"fake-png"
async def close(self) -> None:
self.closed = True
@@ -1327,7 +1333,7 @@ async def test_clear_cart_empty_cart_returns_success_without_clicks():
"""场景 1:购物车没有商品 —— 找不到「削除」按钮立即结束,按清理成功返回
空车时 count API 返回 status=101(合法空车),cart_count 必须是 0 而不是
-1(2026-08-16 修复过的误判);removed_count=0,最终页面 HTML 照常带回。
-1(2026-08-16 修复过的误判);removed_count=0,最终页面 HTML 与截图照常带回。
"""
site = _build_clear_cart_site(
delete_buttons=0, count_body=_EMPTY_COUNT_BODY, html="<html>empty cart</html>",
@@ -1335,7 +1341,12 @@ async def test_clear_cart_empty_cart_returns_success_without_clicks():
result = await site.clear_cart()
assert result == {"removed_count": 0, "cart_count": 0, "html": "<html>empty cart</html>"}
assert result == {
"removed_count": 0,
"cart_count": 0,
"html": "<html>empty cart</html>",
"screenshot": b"fake-png",
}
assert site._context.page.closed is True # 页面必须关,不能泄漏
@@ -1343,7 +1354,7 @@ async def test_clear_cart_with_items_clicks_until_no_button_left():
"""场景 2:购物车有商品 —— 循环点第一个「削除」,点一个少一个,直到按钮消失
每次循环都重新查 selector(避免索引漂移),3 件商品应点 3 次;清空后 count
API 回 status=101 → cart_count=0,最终页面 HTML 进返回值。
API 回 status=101 → cart_count=0,最终页面 HTML 与截图进返回值。
"""
site = _build_clear_cart_site(
delete_buttons=3, count_body=_EMPTY_COUNT_BODY, html="<html>cleared cart</html>",
@@ -1354,11 +1365,12 @@ async def test_clear_cart_with_items_clicks_until_no_button_left():
assert result["removed_count"] == 3
assert result["cart_count"] == 0
assert result["html"] == "<html>cleared cart</html>"
assert result["screenshot"] == b"fake-png"
assert site._context.page.closed is True
async def test_clear_cart_html_capture_failure_keeps_clear_result():
"""最终页面 HTML 抓取失败(如页面异常)不掩盖清理结果本身:html 记空"""
"""最终页面 HTML / 截图抓取失败(如页面异常)不掩盖清理结果本身:两者记空"""
site = _build_clear_cart_site(
delete_buttons=0, count_body=_EMPTY_COUNT_BODY, html="",
)
@@ -1366,11 +1378,15 @@ async def test_clear_cart_html_capture_failure_keeps_clear_result():
async def _broken_content() -> str:
raise RuntimeError("page already closed")
async def _broken_screenshot(*, full_page: bool = True) -> bytes:
raise RuntimeError("page already closed")
site._context.page.content = _broken_content # type: ignore[assignment]
site._context.page.screenshot = _broken_screenshot # type: ignore[assignment]
result = await site.clear_cart()
assert result == {"removed_count": 0, "cart_count": 0, "html": ""}
assert result == {"removed_count": 0, "cart_count": 0, "html": "", "screenshot": b""}
# ---- helper ----