SimpleChatbot / hermes_overlay /tests /test_ds4_merge_nested_data.py
Amin
Deploy: HermesFace finalized project to HF Space
2e658e7
Raw
History Blame Contribute Delete
7.13 kB
"""Focused tests for the DS4 merge-nesting fix in dual_datasource_client.py.
Verifies: (1) DS4-owned fields are read from ds4["data"], not ds4 top level;
(2) DS2 fallback still fires only when a DS4 data field is genuinely
missing; (3) DS2 still cannot override a present/valid DS4 field once read
from the correct nested path; (4) top-level metadata (dataState,
noTradeGuard) is unaffected by the fix, since it was never part of the bug.
Mocked HTTP only -- no live network calls.
"""
import asyncio
import os
import sys
from datetime import datetime, timezone
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) # hermes_overlay/
import trading.dual_datasource_client as ddc
@pytest.fixture(autouse=True)
def _disable_live_binance(monkeypatch):
async def _unavailable(symbol, needed):
return {}, []
monkeypatch.setattr(ddc.binance_public, "get_binance_public_snapshot", _unavailable)
class _FakeResponse:
def __init__(self, payload, status_code=200):
self._payload = payload
self.status_code = status_code
def raise_for_status(self):
pass
def json(self):
return self._payload
def _make_fake_client(ds4_payload, ds2_payloads=None, ds4_raises=False):
ds2_payloads = ds2_payloads or {}
class _FakeClient:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def get(self, url, timeout=None, **_kwargs):
if "short-hunter/snapshot" in url:
if ds4_raises:
raise RuntimeError("boom")
return _FakeResponse(ds4_payload)
for key, payload in ds2_payloads.items():
if key in url:
return _FakeResponse(payload)
return _FakeResponse({})
return _FakeClient()
def _nested_ds4_payload(**overrides):
"""A realistic DS4 envelope: metadata top-level, market fields under data."""
base = {
"success": True,
"symbol": "BTCUSDT",
"dataState": "live",
"noTradeGuard": False,
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"warnings": [],
"errors": [],
"data": {
# contractType/status let _contract_verification() mark this a
# verified Futures instrument (see docs/DATA_PIPELINE_AND_CONTRACTS.md
# and docs/DEVELOPER_GUIDE.md: DS4 owns Futures verification, and a
# bare {"symbol": ...} is deliberately treated as unverified).
"contract": {"symbol": "XBTUSDTM", "contractType": "PERPETUAL", "status": "TRADING"},
"ticker": {"lastPrice": 64364.4, "change24h": None},
"ohlcv": [
{"timestamp": i, "open": 64360, "high": 64370, "low": 64350,
"close": 64364.4, "volume": 5}
for i in range(4)
],
"orderbook": {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]},
"funding": {"currentFundingRate": -8.2e-05},
"openInterest": {"openInterest": 27000928.0, "history": []},
"indicators": {"rsi14": 49.3},
"sentiment": None,
},
}
base.update(overrides)
return base
def test_merge_reads_futures_fields_from_nested_data(monkeypatch):
payload = _nested_ds4_payload()
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["ticker"] == {"lastPrice": 64364.4, "change24h": None}
assert merged["funding"] == {"currentFundingRate": -8.2e-05}
assert merged["openInterest"] == {"openInterest": 27000928.0, "history": []}
assert merged["orderbook"] == {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]}
assert merged["contract"] == {
"symbol": "XBTUSDTM",
"contractType": "PERPETUAL",
"status": "TRADING",
}
assert result["sources"]["ticker"] == "datasource4"
assert result["sources"]["funding"] == "datasource4"
assert result["noTradeGuard"] is False
def test_ds2_fallback_fires_only_for_genuinely_missing_nested_field(monkeypatch):
payload = _nested_ds4_payload()
payload["data"]["orderbook"] = None # DS4 genuinely missing this field
ds2_payloads = {"trading/orderbook": {"bids": [[1, 1]], "asks": [[1, 1]]}}
monkeypatch.setattr(
ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload, ds2_payloads)
)
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["orderbook"] == {"bids": [[1, 1]], "asks": [[1, 1]]}
assert result["sources"]["orderbook"] == "datasource2"
assert any("orderbook" in w and "Datasource 2 fallback" in w for w in result["warnings"])
# untouched DS4-owned fields still come from DS4, not DS2
assert result["sources"]["ticker"] == "datasource4"
def test_ds2_cannot_override_valid_nested_ds4_orderbook(monkeypatch):
payload = _nested_ds4_payload() # DS4 orderbook present and valid
ds2_payloads = {"trading/orderbook": {"bids": [[999, 1]], "asks": [[999, 1]]}}
monkeypatch.setattr(
ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload, ds2_payloads)
)
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["orderbook"] == {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]}
assert result["sources"]["orderbook"] == "datasource4"
def test_ds4_data_key_missing_entirely_is_treated_as_no_data(monkeypatch):
payload = {
"success": False, "symbol": "BTCUSDT", "dataState": "PARTIAL",
"noTradeGuard": False, "timestamp": "x", "warnings": [], "errors": [],
# no "data" key at all -- malformed response
}
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
assert result["sources"]["ticker"] == "unavailable"
assert result["merged"]["ticker"] is None
assert result["noTradeGuard"] is True # missing critical fields after merge
assert any("Missing required Futures fields" in r for r in result["noTradeReasons"])
def test_top_level_metadata_unaffected_by_nesting_fix(monkeypatch):
"""dataState/noTradeGuard were never part of the bug -- must stay top-level reads."""
payload = _nested_ds4_payload(dataState="PARTIAL")
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
assert result["noTradeGuard"] is True
assert any("dataState='PARTIAL'" in r for r in result["noTradeReasons"])
# Stale DS4 data remains visible in primary for diagnostics, but is not
# silently promoted into the merged current-market view.
assert result["primary"]["data"]["ticker"] == {"lastPrice": 64364.4, "change24h": None}
assert result["merged"]["ticker"] is None
assert result["sources"]["ticker"] == "unavailable"