Files
shaguabijia-app-server/tests/test_compare_proxy.py
T
marco aa34ad7d0b feat(compare): 外卖比价 2 端点透传到 pricebot-backend (food MVP)
仿 coupon_step 在 app/api/v1/compare.py 加纯 body 透传 2 端点(MVP 不鉴权,只读
device_id/trace_id/step 打日志,不做 schema 校验)。client /api/v1/{intent/recognize,
price/step} → backend 去掉 /v1 转发到 PRICEBOT_BASE_URL,接通外卖比价
client→server→pricebot-backend 链路。

- compare.py: _passthrough helper + intent/recognize + price/step
- config.py: PRICEBOT_COMPARE_TIMEOUT_SEC=60(意图识别大上下文 LLM、price/step
  每帧 LLM,比领券 30s 长;对齐客户端 agent ApiClient 的 60s 读超时)
- main.py: 注册 compare_router
- tests/test_compare_proxy.py: 2 端点参数化测试(透传/5xx→502/不可达→502/坏 JSON→400)
- docs/api/{compare-intent-recognize,compare-price-step}.md + 更新 README 索引
- 待办与技术债.md P2 外卖 2 端点标 (2026-05-27);电商 2 个待接(compare.py
  加两行即可)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:29:20 +08:00

105 lines
3.3 KiB
Python

"""/api/v1/intent/recognize 和 /api/v1/price/step 透传端点测试。
mock 掉对 pricebot 的 httpx 调用,验证(两个端点参数化同跑):
1. 无 token 也能用(MVP 不鉴权,同 coupon/step)
2. pricebot 200 → 响应原样透传,请求 body 原样转发到对应上游路径(去掉 /v1)
3. pricebot 5xx → 502
4. pricebot 网络错误 → 502
5. 非 JSON body → 400
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import httpx
import pytest
# (客户端端点, 期望转发到的 pricebot 上游路径)
ENDPOINTS = [
("/api/v1/intent/recognize", "/api/intent/recognize"),
("/api/v1/price/step", "/api/price/step"),
]
def _stub_screen_state() -> dict:
return {
"screen": {"width": 1080, "height": 2340, "density": 3.0},
"foreground": {"package": "com.sankuai.meituan", "activity": ""},
"windows": [],
}
def _stub_body() -> dict:
return {
"device_id": "test-device",
"trace_id": "test-trace-1",
"step": 0,
"query": "海底捞",
"screen_state": _stub_screen_state(),
}
@pytest.mark.parametrize("path,upstream", ENDPOINTS)
def test_passes_body_through(client, path, upstream) -> None:
"""无 token + pricebot 200 → 响应原样透传,body 原样转发,URL 去掉 /v1。"""
fake_resp = {
"success": True,
"action": {"command": "wait", "params": {"duration_ms": 500}},
"continue": True,
}
captured: dict = {}
async def fake_post(self, url, json=None, **kw):
captured["url"] = url
captured["json"] = json
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json = lambda: fake_resp
return mock_resp
with patch.object(httpx.AsyncClient, "post", fake_post):
r = client.post(path, json=_stub_body())
assert r.status_code == 200, r.text
assert r.json() == fake_resp
assert captured["json"] == _stub_body() # body 原样转发(不鉴权,无需 token)
assert captured["url"].endswith(upstream) # /api/v1/xxx → /api/xxx
@pytest.mark.parametrize("path,upstream", ENDPOINTS)
def test_pricebot_5xx_returns_502(client, path, upstream) -> None:
async def fake_post(self, url, json=None, **kw):
mock_resp = MagicMock()
mock_resp.status_code = 503
mock_resp.text = "service unavailable"
return mock_resp
with patch.object(httpx.AsyncClient, "post", fake_post):
r = client.post(path, json=_stub_body())
assert r.status_code == 502
assert "pricebot upstream returned 503" in r.json()["detail"]
@pytest.mark.parametrize("path,upstream", ENDPOINTS)
def test_pricebot_unreachable_returns_502(client, path, upstream) -> None:
async def fake_post(self, url, json=None, **kw):
raise httpx.ConnectError("connection refused")
with patch.object(httpx.AsyncClient, "post", fake_post):
r = client.post(path, json=_stub_body())
assert r.status_code == 502
assert "pricebot upstream unreachable" in r.json()["detail"]
@pytest.mark.parametrize("path,upstream", ENDPOINTS)
def test_invalid_json_body(client, path, upstream) -> None:
r = client.post(
path,
headers={"Content-Type": "application/json"},
content=b"not-json",
)
assert r.status_code == 400
assert "invalid json body" in r.json()["detail"]