feat(mcp+runtime): allineamento a Cerbero MCP V2 e flag operativi
Adegua Cerbero Bite alla nuova versione 2.0.0 del server MCP unificato (testnet/mainnet routing per token, header X-Bot-Tag obbligatorio) e introduce due interruttori operativi indipendenti per separare la raccolta dati dall'esecuzione di strategia. Auth e collegamento MCP - Token bearer letto dalla nuova variabile CERBERO_BITE_MCP_TOKEN; il valore sceglie l'ambiente upstream (testnet vs mainnet) sul server. Rimosso il caricamento da file (`secrets/core.token`, CERBERO_BITE_CORE_TOKEN_FILE, Docker secret /run/secrets/core_token). - Aggiunto header X-Bot-Tag (default `BOT__CERBERO_BITE`, override via CERBERO_BITE_MCP_BOT_TAG) su ogni call MCP, con validazione lato client (non vuoto, ≤ 64 caratteri). - Cartella `secrets/` rimossa, `.gitignore` ripulito, Dockerfile e docker-compose.yml aggiornati con env passthrough e fail-fast quando manca il token. Modalità operativa (RuntimeFlags) - Nuovo modulo `config/runtime_flags.py` con `RuntimeFlags( data_analysis_enabled, strategy_enabled)` e loader che parserizza CERBERO_BITE_ENABLE_DATA_ANALYSIS e CERBERO_BITE_ENABLE_STRATEGY (true/false/yes/no/on/off/enabled/disabled, case-insensitive). - L'orchestratore espone i flag, audita e logga la modalità al boot (`engine started: env=… data_analysis=… strategy=…`), e in `install_scheduler` esclude i job `entry`/`monitor` quando strategy è off e il job `market_snapshot` quando data analysis è off. I job di infrastruttura (health, backup, manual_actions) restano sempre attivi. - Default profile = "solo analisi dati" (data_analysis=true, strategy=false), pensato per la finestra di soak post-deploy. GUI saldi - `gui/live_data.py::_fetch_deribit_currency` riconosce il campo soft `error` nel payload V2 (HTTP 200 con `error` valorizzato dal server quando l'auth Deribit fallisce) e lo propaga come `BalanceRow.error`, evitando di mostrare un fuorviante equity = 0,00. CLI - Sostituita l'opzione `--token-file` con `--token` (stringa) sui comandi start/dry-run/ping; il default proviene dall'env. Le chiamate al builder dell'orchestrator passano anche `bot_tag` e `flags`. Documentazione - `docs/04-mcp-integration.md`: descrizione del nuovo flusso di auth V2 (token = ambiente, X-Bot-Tag nell'audit) e router unificati. - `docs/06-operational-flow.md`: nuova sezione "Modalità operativa" con i tre profili canonici e tabella di gating per ogni job; aggiunto `market_snapshot` al cron summary. - `docs/10-config-spec.md`: nuova sezione "Variabili d'ambiente" tabellare con tutti gli env, comprese le bool dei flag operativi. - `docs/02-architecture.md`: layout del repo aggiornato (`secrets/` rimosso, `runtime_flags.py` aggiunto), descrizione di `config/` estesa. Test - 5 nuovi test su `_fetch_deribit_currency` (soft-error, payload pulito, eccezione, error blank, signature parity). - 7 nuovi test su `load_runtime_flags` (default, override, parsing truthy/falsy, blank fallback, valore invalido). - 4 nuovi test su `HttpToolClient` (X-Bot-Tag default e custom, blank e troppo lungo rifiutati). - 3 nuovi test integration sull'orchestratore (gating dei job in base ai flag). - Test esistenti su token/CLI ping/orchestrator aggiornati al nuovo schema. Suite intera: 404 passed, 1 skipped (sqlite3 CLI assente sull'host di sviluppo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user