"""D-4a 集成测试:sanguo → bridge 真实 HTTP round-trip(mock bridge server)。 不依赖 Windows——本地起 mock HTTP server 模拟 bridge(health/order/account/positions + 鉴权), 验证 BridgeClient 真发 HTTP(urlopen + X-Bridge-Token + JSON 解析)+ _shadow_trades_to_bridge 完整链路在真实 HTTP 环境跑通。 验证范围(D-4a sanguo 半边): - bridge_client HTTP 层真发请求、带鉴权 header、解析响应 - _shadow_trades_to_bridge → bridge_client → 真 HTTP → mock bridge 收到下单 → paper_shadow_orders 未覆盖(D-4a 另一半,物理依赖 Windows): - bridge → xtquant → miniQMT 真报单(miniQMT 只 Windows,需 Windows bridge 实跑) """ import json import threading from http.server import BaseHTTPRequestHandler, HTTPServer from types import SimpleNamespace from urllib.parse import urlparse from sanguo_trader.bridge_client import BridgeClient from sanguo_trader.live_orchestrator import _shadow_trades_to_bridge from sanguo_trader.persistence import init_db, is_trade_shadowed, save_trade _TOKEN = "integration-test-token" class _MockBridgeHandler(BaseHTTPRequestHandler): """模拟 Windows bridge 的 4 接口 + token 鉴权(契约同 sanguo_qmt_bridge/bridge.py)。""" def log_message(self, *args): # 静默测试日志 pass def _has_token(self) -> bool: return self.headers.get("X-Bridge-Token") == _TOKEN def _respond(self, code: int, payload: dict) -> None: body = json.dumps(payload).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): path = urlparse(self.path).path if path == "/health": self._respond(200, {"status": "ok", "miniqmt_connected": True}) elif path in ("/account", "/positions"): if not self._has_token(): self._respond(401, {"detail": "token 无效"}) return self._respond(200, {"ok": True, "cash": 1e7, "frozen": 0.0, "market_value": 0.0, "total": 1e7, "positions": []}) else: self._respond(404, {"detail": "not found"}) def do_POST(self): path = urlparse(self.path).path if path != "/order": self._respond(404, {"detail": "not found"}) return if not self._has_token(): self._respond(401, {"detail": "token 无效"}) return length = int(self.headers.get("Content-Length", 0)) req = json.loads(self.rfile.read(length)) self.server.orders.append(req) # 记录,供测试断言 self._respond(200, {"ok": True, "order_id": 99999}) class _MockBridge: """mock bridge HTTP server(后台线程)。""" def __init__(self): self.server = HTTPServer(("127.0.0.1", 0), _MockBridgeHandler) self.server.orders = [] self.port = self.server.server_address[1] self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) def start(self) -> "_MockBridge": self.thread.start() return self def stop(self) -> None: self.server.shutdown() @property def url(self) -> str: return f"http://127.0.0.1:{self.port}" @property def orders(self) -> list: """mock bridge 收到的下单请求(handler 写入 server.orders)。""" return self.server.orders class TestBridgeClientRealHttp: """BridgeClient 真发 HTTP round-trip(验证 HTTP 层 + 鉴权 + 解析)。""" def test_get_account_with_token(self): b = _MockBridge().start() try: r = BridgeClient(b.url, _TOKEN).get_account() assert r is not None and r["ok"] is True and r["cash"] == 1e7 finally: b.stop() def test_order_real_http_payload(self): b = _MockBridge().start() try: r = BridgeClient(b.url, _TOKEN).place_order("sh600000", "buy", 10.5, 100) assert r == {"ok": True, "order_id": 99999} assert b.orders == [{"code": "sh600000", "action": "buy", "price": 10.5, "volume": 100, "price_type": "limit", "reason": ""}] finally: b.stop() def test_wrong_token_returns_none_on_401(self): b = _MockBridge().start() try: # 错 token → mock 返回 401 → urlopen 抛 HTTPError → bridge_client 返回 None r = BridgeClient(b.url, "wrong-token").get_account() assert r is None finally: b.stop() def _cfg(url: str) -> SimpleNamespace: return SimpleNamespace(live={"enabled": True, "bridge_url": url, "shadow": True}) class TestShadowTradesRealHttp: """_shadow_trades_to_bridge 真发 HTTP 到 mock bridge(D-4a sanguo 半边端到端)。""" def test_full_shadow_roundtrip(self, tmp_path, monkeypatch): db = str(tmp_path / "t.db") init_db(db) trade_id = save_trade(db, 1, {"symbol": "600000", "direction": "buy", "price": 10.5, "volume": 100, "bar_date": "2026-07-11", "strategy_id": "s1"}) monkeypatch.setenv("BRIDGE_TOKEN", _TOKEN) b = _MockBridge().start() try: _shadow_trades_to_bridge(db, 1, "2026-07-11", _cfg(b.url)) # mock bridge 真收到下单(symbol 已转 sh600000) assert len(b.orders) == 1 assert b.orders[0]["code"] == "sh600000" assert b.orders[0]["action"] == "buy" # paper_shadow_orders 真记录 assert is_trade_shadowed(db, 1, trade_id) finally: b.stop()