diff --git a/tests/tools/test_confirmation_daemon.py b/tests/tools/test_confirmation_daemon.py new file mode 100644 index 000000000..c4e5b6897 --- /dev/null +++ b/tests/tools/test_confirmation_daemon.py @@ -0,0 +1,190 @@ +"""Tests for tools.confirmation_daemon — Human Confirmation Firewall.""" + +import pytest +import time +from tools.confirmation_daemon import ( + ConfirmationDaemon, + ConfirmationRequest, + ConfirmationStatus, + RiskLevel, + classify_action, + _is_whitelisted, + _DEFAULT_WHITELIST, +) + + +class TestClassifyAction: + """Test action risk classification.""" + + def test_crypto_tx_is_critical(self): + assert classify_action("crypto_tx") == RiskLevel.CRITICAL + + def test_sign_transaction_is_critical(self): + assert classify_action("sign_transaction") == RiskLevel.CRITICAL + + def test_send_email_is_high(self): + assert classify_action("send_email") == RiskLevel.HIGH + + def test_send_message_is_medium(self): + assert classify_action("send_message") == RiskLevel.MEDIUM + + def test_access_calendar_is_low(self): + assert classify_action("access_calendar") == RiskLevel.LOW + + def test_unknown_action_is_medium(self): + assert classify_action("unknown_action_xyz") == RiskLevel.MEDIUM + + +class TestWhitelist: + """Test whitelist auto-approval.""" + + def test_self_email_is_whitelisted(self): + whitelist = dict(_DEFAULT_WHITELIST) + payload = {"from": "me@test.com", "to": "me@test.com"} + assert _is_whitelisted("send_email", payload, whitelist) is True + + def test_non_whitelisted_recipient_not_approved(self): + whitelist = dict(_DEFAULT_WHITELIST) + payload = {"to": "random@stranger.com"} + assert _is_whitelisted("send_email", payload, whitelist) is False + + def test_whitelisted_contact_approved(self): + whitelist = { + "send_message": {"targets": ["alice", "bob"]}, + } + assert _is_whitelisted("send_message", {"to": "alice"}, whitelist) is True + assert _is_whitelisted("send_message", {"to": "charlie"}, whitelist) is False + + def test_no_whitelist_entry_means_not_whitelisted(self): + whitelist = {} + assert _is_whitelisted("crypto_tx", {"amount": 1.0}, whitelist) is False + + +class TestConfirmationRequest: + """Test the request data model.""" + + def test_defaults(self): + req = ConfirmationRequest( + request_id="test-1", + action="send_email", + description="Test email", + risk_level="high", + payload={}, + ) + assert req.status == ConfirmationStatus.PENDING.value + assert req.created_at > 0 + assert req.expires_at > req.created_at + + def test_is_pending(self): + req = ConfirmationRequest( + request_id="test-2", + action="send_email", + description="Test", + risk_level="high", + payload={}, + expires_at=time.time() + 300, + ) + assert req.is_pending is True + + def test_is_expired(self): + req = ConfirmationRequest( + request_id="test-3", + action="send_email", + description="Test", + risk_level="high", + payload={}, + expires_at=time.time() - 10, + ) + assert req.is_expired is True + assert req.is_pending is False + + def test_to_dict(self): + req = ConfirmationRequest( + request_id="test-4", + action="send_email", + description="Test", + risk_level="medium", + payload={"to": "a@b.com"}, + ) + d = req.to_dict() + assert d["request_id"] == "test-4" + assert d["action"] == "send_email" + assert "is_pending" in d + + +class TestConfirmationDaemon: + """Test the daemon logic (without HTTP layer).""" + + def test_auto_approve_low_risk(self): + daemon = ConfirmationDaemon() + req = daemon.request( + action="access_calendar", + description="Read today's events", + risk_level="low", + ) + assert req.status == ConfirmationStatus.AUTO_APPROVED.value + + def test_whitelisted_auto_approves(self): + daemon = ConfirmationDaemon() + daemon._whitelist = {"send_message": {"targets": ["alice"]}} + req = daemon.request( + action="send_message", + description="Message alice", + payload={"to": "alice"}, + ) + assert req.status == ConfirmationStatus.AUTO_APPROVED.value + + def test_non_whitelisted_goes_pending(self): + daemon = ConfirmationDaemon() + daemon._whitelist = {} + req = daemon.request( + action="send_email", + description="Email to stranger", + payload={"to": "stranger@test.com"}, + risk_level="high", + ) + assert req.status == ConfirmationStatus.PENDING.value + assert req.is_pending is True + + def test_approve_response(self): + daemon = ConfirmationDaemon() + daemon._whitelist = {} + req = daemon.request( + action="send_email", + description="Email test", + risk_level="high", + ) + result = daemon.respond(req.request_id, approved=True, decided_by="human") + assert result.status == ConfirmationStatus.APPROVED.value + assert result.decided_by == "human" + + def test_deny_response(self): + daemon = ConfirmationDaemon() + daemon._whitelist = {} + req = daemon.request( + action="crypto_tx", + description="Send 1 ETH", + risk_level="critical", + ) + result = daemon.respond( + req.request_id, approved=False, decided_by="human", reason="Too risky" + ) + assert result.status == ConfirmationStatus.DENIED.value + assert result.reason == "Too risky" + + def test_get_pending(self): + daemon = ConfirmationDaemon() + daemon._whitelist = {} + daemon.request(action="send_email", description="Test 1", risk_level="high") + daemon.request(action="send_email", description="Test 2", risk_level="high") + pending = daemon.get_pending() + assert len(pending) >= 2 + + def test_get_history(self): + daemon = ConfirmationDaemon() + req = daemon.request( + action="access_calendar", description="Test", risk_level="low" + ) + history = daemon.get_history() + assert len(history) >= 1 + assert history[0]["action"] == "access_calendar"