比价记录改后端 harvest: 透传壳落库替代客户端主动 POST
compare.py 透传壳新增后端 harvest(不再依赖客户端 POST /compare/record): - 帧0(客户端首帧不带 trace_id)由 app-server 用 uuid 签发 trace_id、注入转发 body、 回填响应顶层,并按 trace_id 建 running 行(harvest_running) - done 帧(command=done 且 continue=false)更新为 success/failed 终态,并对本次新落成 success 幂等发一次邀请奖(harvest_done + try_reward_on_compare) - /trace/finalize(用户终止/Phase1 未识别,无 done)更新为 cancelled/failed, 不降级已 success(harvest_abort) - 软鉴权 OptionalUser(deps.py 新增 get_current_user_optional):新客户端带 JWT 绑 user_id,老客户端匿名建行、user_id 暂空,由其后续 POST /compare/record 按 trace_id reconcile;新版覆盖够高后可收紧成硬鉴权 - 结构化日志(logging.py 加 trace_id_ctx ContextVar):请求级 trace_id 贯穿 harvest 各阶段 comparison_record 表结构调整(迁移 comparison_record_trace_unique): - 唯一键 (user_id,trace_id) → trace_id 单列(harvest 帧0 建行时 user_id 可能暂缺) - user_id 改可空 repositories/comparison.py 加 harvest_running/harvest_done/harvest_abort; compare_record.py 的 POST /record 降级为灰度期老客户端兼容(按 trace_id 幂等、不降级 success); 补 tests/test_compare_harvest.py,test_compare_proxy/milestone 适配。 文档同步: docs/database/comparison_record.md 更新表结构(唯一键/user_id 可空/status 取值) 与写入模型; docs/api/compare/compare-record-report.md 标注 POST /record 灰度定位。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ mock 掉对 pricebot 的 httpx 调用,验证(两个端点参数化同跑):
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
@@ -41,7 +42,8 @@ def _stub_body() -> dict:
|
||||
|
||||
@pytest.mark.parametrize("path,upstream", ENDPOINTS)
|
||||
def test_passes_body_through(client, path, upstream) -> None:
|
||||
"""无 token + pricebot 200 → 响应原样透传,body 原样转发,URL 去掉 /v1。"""
|
||||
"""无 token(软鉴权放行)+ pricebot 200 → 响应透传(+ app-server 注入 trace_id),
|
||||
body 原样转发(自带 trace_id 时不重签),URL 去掉 /v1。"""
|
||||
fake_resp = {
|
||||
"success": True,
|
||||
"action": {"command": "wait", "params": {"duration_ms": 500}},
|
||||
@@ -49,20 +51,25 @@ def test_passes_body_through(client, path, upstream) -> None:
|
||||
}
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_post(self, url, json=None, **kw):
|
||||
async def fake_post(self, url, content=None, **kw):
|
||||
# 透传壳用 content=raw(bytes)转发,不是 json=;这里捕获 content 解回来核对
|
||||
captured["url"] = url
|
||||
captured["json"] = json
|
||||
captured["content"] = content
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json = lambda: fake_resp
|
||||
mock_resp.json = lambda: dict(fake_resp) # 每次新副本(端点会 setdefault trace_id 进去)
|
||||
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)
|
||||
body = r.json()
|
||||
assert body["success"] is True
|
||||
assert body["action"] == fake_resp["action"]
|
||||
assert body["trace_id"] == "test-trace-1" # 自带 trace_id → 原样回传, 不 mint
|
||||
# body 原样转发(content=raw bytes;自带 trace_id 时不重新序列化)
|
||||
assert json.loads(captured["content"]) == _stub_body()
|
||||
assert captured["url"].endswith(upstream) # /api/v1/xxx → /api/xxx
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user