from __future__ import annotations import pytest from pydantic import BaseModel class FakeReq(BaseModel): instrument_name: str qty: float @pytest.mark.asyncio async def test_audit_call_logs_success(monkeypatch): from cerbero_mcp.common.audit_helpers import audit_call logged = [] def fake_audit(**kw): logged.append(kw) monkeypatch.setattr("cerbero_mcp.common.audit_helpers.audit_write_op", fake_audit) class FakeRequest: class _State: environment = "testnet" state = _State() async def tool_fn(): return {"order_id": "abc123", "state": "filled"} result = await audit_call( request=FakeRequest(), # type: ignore[arg-type] exchange="deribit", action="place_order", target_field="instrument_name", params=FakeReq(instrument_name="BTC-PERPETUAL", qty=0.1), tool_fn=tool_fn, ) assert result == {"order_id": "abc123", "state": "filled"} assert len(logged) == 1 rec = logged[0] assert rec["actor"] == "testnet" assert rec["exchange"] == "deribit" assert rec["action"] == "place_order" assert rec["target"] == "BTC-PERPETUAL" assert rec["payload"]["qty"] == 0.1 assert rec["result"]["order_id"] == "abc123" assert "error" not in rec or rec.get("error") is None @pytest.mark.asyncio async def test_audit_call_logs_error_and_reraises(monkeypatch): from cerbero_mcp.common.audit_helpers import audit_call logged = [] def fake_audit(**kw): logged.append(kw) monkeypatch.setattr("cerbero_mcp.common.audit_helpers.audit_write_op", fake_audit) class FakeRequest: class _State: environment = "mainnet" state = _State() async def tool_fn(): raise RuntimeError("upstream timeout") with pytest.raises(RuntimeError, match="upstream timeout"): await audit_call( request=FakeRequest(), # type: ignore[arg-type] exchange="deribit", action="cancel_order", target_field="instrument_name", params=FakeReq(instrument_name="BTC-PERPETUAL", qty=0.0), tool_fn=tool_fn, ) assert len(logged) == 1 rec = logged[0] assert rec["actor"] == "mainnet" assert "RuntimeError: upstream timeout" in rec["error"] @pytest.mark.asyncio async def test_audit_call_no_params_no_target(): from cerbero_mcp.common.audit_helpers import audit_call class FakeRequest: class _State: environment = "testnet" state = _State() async def tool_fn(): return {"ok": True} result = await audit_call( request=FakeRequest(), # type: ignore[arg-type] exchange="bybit", action="cancel_all_orders", tool_fn=tool_fn, ) assert result == {"ok": True} @pytest.mark.asyncio async def test_audit_call_propagates_bot_tag(monkeypatch): """bot_tag letto da request.state e propagato a audit_write_op.""" from cerbero_mcp.common.audit_helpers import audit_call logged = [] def fake_audit(**kw): logged.append(kw) monkeypatch.setattr("cerbero_mcp.common.audit_helpers.audit_write_op", fake_audit) class FakeRequest: class _State: environment = "testnet" bot_tag = "scanner-alpha" state = _State() async def tool_fn(): return {"order_id": "abc"} await audit_call( request=FakeRequest(), # type: ignore[arg-type] exchange="deribit", action="place_order", target_field="instrument_name", params=FakeReq(instrument_name="BTC-PERPETUAL", qty=0.1), tool_fn=tool_fn, ) assert len(logged) == 1 assert logged[0]["bot_tag"] == "scanner-alpha" assert logged[0]["actor"] == "testnet" @pytest.mark.asyncio async def test_audit_call_bot_tag_none_when_missing(monkeypatch): """Se request.state.bot_tag non esiste, audit riceve None senza errore.""" from cerbero_mcp.common.audit_helpers import audit_call logged = [] def fake_audit(**kw): logged.append(kw) monkeypatch.setattr("cerbero_mcp.common.audit_helpers.audit_write_op", fake_audit) class FakeRequest: class _State: environment = "testnet" state = _State() async def tool_fn(): return {"ok": True} await audit_call( request=FakeRequest(), # type: ignore[arg-type] exchange="bybit", action="cancel_all_orders", tool_fn=tool_fn, ) assert len(logged) == 1 assert logged[0]["bot_tag"] is None