|
8 | 8 | window (no message in the timeout) is a PASS — the value is that connect + |
9 | 9 | auth + SUBSCRIBE succeed without raising; when a message does arrive its shape |
10 | 10 | is still checked. |
11 | | -- High-traffic (continuous): `/ticker` and `/premium`. These emit non-stop, so |
12 | | - a quiet window is a FAIL — the test asserts a real, well-formed data payload |
13 | | - actually arrives, proving the SDK decodes and yields end-to-end. |
| 11 | +- Guaranteed-data: `/ticker` and `/premium` emit non-stop; `/liquidation/feed` |
| 12 | + replays the last N liquidations immediately on connect. Either way a fresh |
| 13 | + connection yields data, so a quiet window is a FAIL — the test asserts a real, |
| 14 | + well-formed data payload actually arrives, proving the SDK decodes and yields |
| 15 | + end-to-end. |
14 | 16 |
|
15 | 17 | Opt-in and deselected from the keyless CI lane via the `integration` marker; |
16 | 18 | needs DATAMAXI_API_KEY. Skipped when `websockets` absent. |
|
41 | 43 | # /forex is quiet-tolerant but its first tick can lag (~14s observed); wait a bit |
42 | 44 | # longer than _QUIET_TIMEOUT so an arriving tick gets shape-checked, not skipped. |
43 | 45 | _FOREX_TIMEOUT = 20.0 |
| 46 | +# /liquidation/feed replays the last N liquidations immediately on connect, so |
| 47 | +# data lands within the handshake — a short timeout suffices and a quiet window |
| 48 | +# is a FAIL. Number of leading frames to collect for the snap-replay assertion. |
| 49 | +_REPLAY_TIMEOUT = 10.0 |
| 50 | +_REPLAY_FRAMES = 5 |
44 | 51 |
|
45 | 52 |
|
46 | 53 | def _run(coro): |
@@ -141,6 +148,39 @@ async def run(): |
141 | 148 | assert "premium" in msg |
142 | 149 |
|
143 | 150 |
|
| 151 | +def test_ws_liquidation_feed_replays_on_connect(): |
| 152 | + # /liquidation/feed is a connect-only firehose (no SUBSCRIBE, no params). The |
| 153 | + # backend replays the last N liquidations immediately on connect (each frame |
| 154 | + # tagged `snap: true`), so a fresh connection deterministically yields data |
| 155 | + # and — like ticker — a quiet window is a FAIL. A live event may interleave |
| 156 | + # between replayed frames, so the first frame is not guaranteed to be replay; |
| 157 | + # assert `snap: true` on at least one of the first few frames, not frame[0]. |
| 158 | + # (This couples the test to replay being enabled: with |
| 159 | + # LIQUIDATION_FEED_REPLAY_N=0 in the target env, it FAILs rather than skips — |
| 160 | + # deliberately, so an accidentally-disabled replay is caught.) |
| 161 | + async def run(): |
| 162 | + async with AsyncDatamaxiWS(api_key=API_KEY, base_url=BASE_URL) as ws: |
| 163 | + stream = await ws.liquidation_feed.stream() |
| 164 | + frames = [] |
| 165 | + try: |
| 166 | + while len(frames) < _REPLAY_FRAMES: |
| 167 | + frames.append( |
| 168 | + await asyncio.wait_for(stream.__anext__(), _REPLAY_TIMEOUT) |
| 169 | + ) |
| 170 | + except asyncio.TimeoutError: |
| 171 | + pass # fewer than N in the ring; keep what arrived |
| 172 | + return frames |
| 173 | + |
| 174 | + frames = _run(run()) |
| 175 | + assert frames, "no liquidation frame within timeout (replay disabled?)" |
| 176 | + first = frames[0] |
| 177 | + assert isinstance(first, dict) |
| 178 | + for field in ("e", "d", "s", "b", "q", "sd"): |
| 179 | + assert field in first, f"missing {field!r} in {first!r}" |
| 180 | + assert int(first["d"]) > 1_000_000_000_000 # plausible UNIX-ms (str or int) |
| 181 | + assert any(f.get("snap") is True for f in frames) |
| 182 | + |
| 183 | + |
144 | 184 | def test_ws_forex_tolerates_quiet_window(): |
145 | 185 | # /forex is continuous during FX market hours but the first tick can lag |
146 | 186 | # (~14s observed) and FX spot does not trade weekends — so a quiet window is |
|
0 commit comments