"""Tests for the GUI live-balances fetcher (soft-error handling).""" from __future__ import annotations from decimal import Decimal from typing import Any import pytest from cerbero_bite.clients.deribit import DeribitClient from cerbero_bite.gui.live_data import _fetch_deribit_currency class _FakeDeribit: def __init__(self, payload: dict[str, Any] | Exception) -> None: self._payload = payload async def get_account_summary(self, currency: str) -> dict[str, Any]: del currency # not used by the fake; kept for signature parity if isinstance(self._payload, Exception): raise self._payload return self._payload @pytest.mark.asyncio async def test_soft_error_payload_becomes_row_error() -> None: """MCP V2 returns 200 + ``error`` field when upstream auth fails.""" fake = _FakeDeribit( { "equity": 0, "balance": 0, "available_funds": 0, "unrealized_pnl": 0, "error": "Deribit auth failed (code=13004): invalid_credentials", } ) row = await _fetch_deribit_currency( deribit=fake, # type: ignore[arg-type] currency="USDC", ) assert row.exchange == "deribit" assert row.currency == "USDC" assert row.equity is None assert row.available is None assert row.unrealized_pnl is None assert row.error is not None assert "invalid_credentials" in row.error @pytest.mark.asyncio async def test_clean_payload_populates_balance_fields() -> None: fake = _FakeDeribit( { "equity": "12.5", "available_funds": "10.0", "unrealized_pnl": "-0.25", } ) row = await _fetch_deribit_currency( deribit=fake, # type: ignore[arg-type] currency="USDC", ) assert row.error is None assert row.equity == Decimal("12.5") assert row.available == Decimal("10.0") assert row.unrealized_pnl == Decimal("-0.25") @pytest.mark.asyncio async def test_exception_becomes_row_error() -> None: fake = _FakeDeribit(RuntimeError("boom")) row = await _fetch_deribit_currency( deribit=fake, # type: ignore[arg-type] currency="USDC", ) assert row.equity is None assert row.error is not None assert "RuntimeError" in row.error assert "boom" in row.error @pytest.mark.asyncio async def test_blank_error_field_is_ignored() -> None: """An ``error`` field that is empty/None must not trigger the soft-error path.""" fake = _FakeDeribit( {"equity": "1.0", "available_funds": "1.0", "unrealized_pnl": "0.0", "error": None} ) row = await _fetch_deribit_currency( deribit=fake, # type: ignore[arg-type] currency="USDC", ) assert row.error is None assert row.equity == Decimal("1.0") # Sanity-check: the production class signature is what we expect to be drop-in # replaceable by ``_FakeDeribit``. def test_fake_matches_production_signature() -> None: assert hasattr(DeribitClient, "get_account_summary")