"""D-3 影子下单集成测试(persistence 幂等 + _shadow_trades_to_bridge 正向路径)。 不依赖 Windows——mock BridgeClient.place_order,验证: - persistence 幂等(save_shadow_order / is_trade_shadowed) - enabled=false 跳过(不下单、不查库) - enabled=true + bridge ok → paper_shadow_orders 记录 + symbol 转换正确 - 幂等:重跑同 trade_id 不重复下单 - bridge 失败 → status=failed,不阻断 """ import sqlite3 from types import SimpleNamespace from unittest.mock import patch from sanguo_trader.live_orchestrator import _shadow_trades_to_bridge from sanguo_trader.persistence import ( init_db, is_trade_shadowed, save_shadow_order, save_trade, ) _DATE = "2026-07-11" def _mk_db(tmp_path) -> str: db = str(tmp_path / "t.db") init_db(db) return db def _cfg(enabled: bool) -> SimpleNamespace: return SimpleNamespace(live={ "enabled": enabled, "bridge_url": "http://b.test", "shadow": True, }) def _insert_trade(db, account_id=1, symbol="600000", direction="buy") -> int: """插一条当日成交,返回 trade_id。""" return save_trade(db, account_id, { "symbol": symbol, "direction": direction, "price": 10.5, "volume": 100, "bar_date": _DATE, "strategy_id": "s1", }) class TestShadowPersistence: def test_idempotent_roundtrip(self, tmp_path): db = _mk_db(tmp_path) assert not is_trade_shadowed(db, 1, 100) save_shadow_order(db, 1, 100, 555, "ok") assert is_trade_shadowed(db, 1, 100) class TestShadowTradesToBridge: def test_disabled_skips(self, tmp_path): """enabled=false → 不创建 BridgeClient、不下单。""" db = _mk_db(tmp_path) _insert_trade(db) with patch("sanguo_trader.bridge_client.BridgeClient") as mbc: _shadow_trades_to_bridge(db, 1, _DATE, _cfg(False)) mbc.assert_not_called() def test_enabled_records_and_symbol_converted(self, tmp_path): """enabled=true + bridge ok → 记录 paper_shadow_orders + code 转换正确。""" db = _mk_db(tmp_path) trade_id = _insert_trade(db, symbol="600000", direction="buy") with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.place_order.return_value = {"ok": True, "order_id": 777} _shadow_trades_to_bridge(db, 1, _DATE, _cfg(True)) call = mbc.return_value.place_order.call_args assert call[0][0] == "sh600000" # code 已转 sh 前缀 assert call[0][1] == "buy" assert is_trade_shadowed(db, 1, trade_id) def test_idempotent_no_duplicate(self, tmp_path): """同 trade_id 重跑 → 不重复下单。""" db = _mk_db(tmp_path) _insert_trade(db) with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.place_order.return_value = {"ok": True, "order_id": 777} _shadow_trades_to_bridge(db, 1, _DATE, _cfg(True)) assert mbc.return_value.place_order.call_count == 1 # 重跑:trade_id 已 shadowed,不再下单 _shadow_trades_to_bridge(db, 1, _DATE, _cfg(True)) assert mbc.return_value.place_order.call_count == 1 def test_bridge_failure_does_not_block(self, tmp_path): """bridge 返回 None(失败)→ 记 status=failed,不抛异常。""" db = _mk_db(tmp_path) _insert_trade(db) with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.place_order.return_value = None _shadow_trades_to_bridge(db, 1, _DATE, _cfg(True)) # 不抛 with sqlite3.connect(db) as conn: row = conn.execute( "SELECT status FROM paper_shadow_orders WHERE account_id=1" ).fetchone() assert row and row[0] == "failed" class TestShadowTokenSource: """bridge_token 优先级:config > env > skip(D-3 影子下单)。""" def test_config_token_used(self, tmp_path): """(a) config 有 bridge_token → 用 config token(env 未设)。""" db = _mk_db(tmp_path) _insert_trade(db) cfg = SimpleNamespace(live={ "enabled": True, "bridge_url": "http://b.test", "shadow": True, "bridge_token": "cfg-token", }) with patch.dict("os.environ", {}, clear=True), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.place_order.return_value = {"ok": True, "order_id": 1} _shadow_trades_to_bridge(db, 1, _DATE, cfg) assert mbc.call_args[0][1] == "cfg-token" # BridgeClient(url, token) def test_env_fallback_when_config_missing(self, tmp_path): """(b) config 无 bridge_token → fallback 到 BRIDGE_TOKEN env。""" db = _mk_db(tmp_path) _insert_trade(db) cfg = SimpleNamespace(live={ "enabled": True, "bridge_url": "http://b.test", "shadow": True, }) with patch.dict("os.environ", {"BRIDGE_TOKEN": "env-token"}), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.place_order.return_value = {"ok": True, "order_id": 1} _shadow_trades_to_bridge(db, 1, _DATE, cfg) assert mbc.call_args[0][1] == "env-token" def test_no_token_skips(self, tmp_path): """(c) config 和 env 都无 bridge_token → 跳过、warning,不下单。""" db = _mk_db(tmp_path) _insert_trade(db) cfg = SimpleNamespace(live={ "enabled": True, "bridge_url": "http://b.test", "shadow": True, }) with patch.dict("os.environ", {}, clear=True), \ patch("sanguo_trader.bridge_client.BridgeClient") as mbc: _shadow_trades_to_bridge(db, 1, _DATE, cfg) mbc.assert_not_called()