Phase 4 hardening: status CLI, lock file, backup job, hash enforce, pooling, real bias

Sei interventi mirati sui rischi operativi rilevati nell'audit
post-Fase 4. 317 test pass, mypy strict pulito, ruff clean.

1. status CLI: legge SQLite reale e mostra kill_switch, posizioni
   aperte, environment, config_version, last_health_check, started_at.
   Sostituisce il placeholder "phase 0 skeleton".

2. Lock file single-instance: runtime/lockfile.py acquisisce
   data/.lockfile via fcntl.flock al boot di run_forever; un secondo
   container fallisce subito con LockError.

3. Backup orario nello scheduler: nuovo job APScheduler 0 * * * *
   chiama scripts.backup.backup_database + prune_backups.

4. config_hash enforce su start: il CLI start verifica l'integrità
   del file (enforce_hash=True). Mismatch → exit 1 prima di toccare
   stato. dry-run resta enforce_hash=False per debug.

5. Connection pooling MCP: RuntimeContext espone un httpx.AsyncClient
   long-lived condiviso da tutti i wrapper (limits 20/10
   connections/keepalive). aclose() chiamato in run_forever finale.

6. Bias direzionale reale: deribit.historical_close +
   deribit.adx_14 popolano TrendContext con spot a 30 giorni e
   ADX(14) effettivi. Sblocca bull_put e bear_call. Quando i dati
   storici mancano l'engine emette alert MEDIUM e cade su no_entry
   in modo deterministico.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 00:15:28 +02:00
parent ca1e6379df
commit 411b747e93
11 changed files with 439 additions and 36 deletions
+54
View File
@@ -0,0 +1,54 @@
"""Tests for the single-instance EngineLock."""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from cerbero_bite.runtime.lockfile import EngineLock, LockError
def test_acquire_writes_pid(tmp_path: Path) -> None:
target = tmp_path / "lockfile"
with EngineLock(target):
assert target.exists()
content = target.read_text(encoding="utf-8").strip()
assert int(content) == os.getpid()
def test_release_after_with_block(tmp_path: Path) -> None:
target = tmp_path / "lockfile"
lock = EngineLock(target)
with lock:
pass
# second acquire must succeed because the previous one was released
with EngineLock(target):
pass
def test_second_acquire_blocks(tmp_path: Path) -> None:
target = tmp_path / "lockfile"
first = EngineLock(target)
first.acquire()
try:
second = EngineLock(target)
with pytest.raises(LockError, match="another Cerbero Bite instance"):
second.acquire()
finally:
first.release()
def test_lockfile_directory_is_created(tmp_path: Path) -> None:
nested = tmp_path / "data" / "nested" / "lockfile"
with EngineLock(nested):
assert nested.exists()
def test_release_is_idempotent(tmp_path: Path) -> None:
target = tmp_path / "lockfile"
lock = EngineLock(target)
lock.acquire()
lock.release()
lock.release() # must be a no-op