"""Tests for the audit chain anti-truncation anchor.""" from __future__ import annotations from datetime import UTC, datetime from pathlib import Path import pytest from pytest_httpx import HTTPXMock from cerbero_bite.config import golden_config from cerbero_bite.config.mcp_endpoints import load_endpoints from cerbero_bite.runtime import build_runtime from cerbero_bite.runtime.orchestrator import Orchestrator from cerbero_bite.state import connect pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def _now() -> datetime: return datetime(2026, 4, 27, 14, 0, tzinfo=UTC) def _build(tmp_path: Path) -> Orchestrator: ctx = build_runtime( cfg=golden_config(), endpoints=load_endpoints(env={}), token="t", db_path=tmp_path / "state.sqlite", audit_path=tmp_path / "audit.log", retry_max=1, clock=_now, ) return Orchestrator( ctx, expected_environment="testnet", eur_to_usd=__import__("decimal").Decimal("1.075"), ) def _wire_boot_dependencies(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, }, is_reusable=True, ) httpx_mock.add_response( url="http://mcp-deribit:9011/tools/get_positions", json=[], is_reusable=True, ) httpx_mock.add_response( url="http://mcp-macro:9013/tools/get_macro_calendar", json={"events": []}, is_reusable=True, ) httpx_mock.add_response( url="http://mcp-sentiment:9014/tools/get_cross_exchange_funding", json={"snapshot": {}}, is_reusable=True, ) httpx_mock.add_response( url="http://mcp-hyperliquid:9012/tools/get_funding_rate", json={"asset": "ETH", "current_funding_rate": 0.0001}, is_reusable=True, ) @pytest.mark.asyncio async def test_audit_anchor_persisted_after_append(tmp_path: Path) -> None: orch = _build(tmp_path) orch.context.audit_log.append( event="TEST", payload={"x": 1}, now=_now(), ) conn = connect(tmp_path / "state.sqlite") try: state = orch.context.repository.get_system_state(conn) finally: conn.close() assert state is not None assert state.last_audit_hash == orch.context.audit_log.last_hash @pytest.mark.asyncio async def test_boot_detects_audit_truncation( tmp_path: Path, httpx_mock: HTTPXMock ) -> None: orch = _build(tmp_path) # Append three lines so we have something to truncate. for i in range(3): orch.context.audit_log.append( event=f"E{i}", payload={"i": i}, now=_now() ) # Truncate the file: keep only the first line. audit_path = tmp_path / "audit.log" head = audit_path.read_text(encoding="utf-8").splitlines(keepends=True)[0] audit_path.write_text(head, encoding="utf-8") # Rebuild orchestrator (the AuditLog tail-reads the file again). orch = _build(tmp_path) _wire_boot_dependencies(httpx_mock) await orch.boot() assert orch.context.kill_switch.is_armed() is True