Files
Adriano ce158a92dd 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>
2026-05-01 17:14:40 +02:00

93 lines
3.0 KiB
Python

"""End-to-end test for ``cerbero-bite ping``.
The CLI uses the production code paths, so we set up an HTTP mock that
matches every URL Bite is going to hit and assert the rendered output
contains the expected statuses.
"""
from __future__ import annotations
import pytest
from click.testing import CliRunner
from pytest_httpx import HTTPXMock
from cerbero_bite.cli import main as cli_main
def test_ping_reports_each_service(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/environment_info",
json={
"exchange": "deribit",
"environment": "testnet",
"source": "env",
"env_value": "true",
"base_url": "https://test.deribit.com/api/v2",
"max_leverage": 3,
},
)
httpx_mock.add_response(
url="http://mcp-hyperliquid:9012/tools/get_funding_rate",
json={"asset": "ETH", "current_funding_rate": 0.0001},
)
httpx_mock.add_response(
url="http://mcp-macro:9013/tools/get_macro_calendar",
json={"events": []},
)
httpx_mock.add_response(
url="http://mcp-sentiment:9014/tools/get_cross_exchange_funding",
json={"snapshot": {"ETH": {"binance": 0.0001}}},
)
result = CliRunner().invoke(
cli_main, ["ping", "--token", "super-secret", "--timeout", "1.0"]
)
assert result.exit_code == 0, result.output
assert "deribit" in result.output
assert "hyperliquid" in result.output
assert "macro" in result.output
assert "sentiment" in result.output
# Telegram and Portfolio are no longer MCP services and are not
# listed by the ping command.
assert "portfolio" not in result.output
assert "OK" in result.output
def test_ping_reports_failure_when_service_unreachable(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/environment_info",
status_code=500,
text="boom",
)
# Provide successful stubs for the others to keep the call small.
httpx_mock.add_response(
url="http://mcp-hyperliquid:9012/tools/get_funding_rate",
json={"asset": "ETH", "current_funding_rate": 0.0001},
)
httpx_mock.add_response(
url="http://mcp-macro:9013/tools/get_macro_calendar",
json={"events": []},
)
httpx_mock.add_response(
url="http://mcp-sentiment:9014/tools/get_cross_exchange_funding",
json={"snapshot": {"ETH": {"binance": 0.0001}}},
)
result = CliRunner().invoke(
cli_main, ["ping", "--token", "super-secret", "--timeout", "1.0"]
)
assert result.exit_code == 0
assert "FAIL" in result.output
def test_ping_token_missing_exits_nonzero(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Ensure no env var leaks into the CLI invocation.
monkeypatch.delenv("CERBERO_BITE_MCP_TOKEN", raising=False)
result = CliRunner().invoke(cli_main, ["ping"])
assert result.exit_code == 1
assert "token error" in result.output