ForcePilot/backend/test/unit/channels/test_channels_slack_approval.py

336 lines
12 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock
import pytest
from yuxi.channels.adapters.slack.approval import (
ApprovalManager,
ApprovalRequest,
ApprovalStatus,
)
from yuxi.channels.adapters.slack.approval_native import (
ExecApprovalConfig,
is_authorized_sender,
normalize_approver_id,
resolve_origin_target,
)
class TestApprovalStatus:
def test_values(self):
assert ApprovalStatus.PENDING == "pending"
assert ApprovalStatus.APPROVED == "approved"
assert ApprovalStatus.REJECTED == "rejected"
assert ApprovalStatus.EXPIRED == "expired"
assert ApprovalStatus.EXECUTED == "executed"
assert ApprovalStatus.FAILED == "failed"
class TestApprovalRequest:
def test_creation(self):
req = ApprovalRequest(
approval_id="req001",
title="Test Approval",
detail="Approve this action",
command="/do_something",
chat_id="C001",
message_ts="123.456",
created_by="U001",
)
assert req.approval_id == "req001"
assert req.title == "Test Approval"
assert req.detail == "Approve this action"
assert req.command == "/do_something"
assert req.chat_id == "C001"
assert req.message_ts == "123.456"
assert req.created_by == "U001"
assert req.status == ApprovalStatus.PENDING
assert req.approved_by == ""
assert req.exec_result == ""
assert req.exec_error == ""
def test_default_values(self):
req = ApprovalRequest(
approval_id="req002",
title="Test",
detail="Desc",
command="/cmd",
chat_id="C001",
message_ts="123.456",
)
assert req.created_by == ""
assert req.approved_by == ""
assert req.status == ApprovalStatus.PENDING
assert req.exec_result == ""
assert req.exec_error == ""
def test_is_pending(self):
req = ApprovalRequest("r1", "T", "D", "/c", "C1", "ts")
assert req.is_pending is True
req.status = ApprovalStatus.APPROVED
assert req.is_pending is False
def test_is_resolved(self):
req = ApprovalRequest("r1", "T", "D", "/c", "C1", "ts")
assert req.is_resolved is False
req.status = ApprovalStatus.APPROVED
assert req.is_resolved is True
req.status = ApprovalStatus.REJECTED
assert req.is_resolved is True
req.status = ApprovalStatus.EXPIRED
assert req.is_resolved is True
class TestApprovalManager:
@pytest.fixture
def manager(self):
return ApprovalManager(ttl_seconds=600)
def test_create_approval(self, manager):
req = manager.create_approval("Title", "Detail", "/cmd", "C001", "123.456", "U001")
assert req.title == "Title"
assert req.status == ApprovalStatus.PENDING
assert len(req.approval_id) == 12
assert manager.get_approval(req.approval_id) is req
def test_create_approval_unique_ids(self, manager):
req1 = manager.create_approval("T1", "D1", "/c1", "C1", "ts")
req2 = manager.create_approval("T2", "D2", "/c2", "C2", "ts")
assert req1.approval_id != req2.approval_id
def test_get_approval_not_found(self, manager):
assert manager.get_approval("nonexistent") is None
def test_approve(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
updated = manager.approve(req.approval_id, "U001")
assert updated is not None
assert updated.status == ApprovalStatus.APPROVED
assert updated.approved_by == "U001"
def test_approve_not_found(self, manager):
assert manager.approve("nonexistent") is None
def test_approve_already_resolved(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
manager.approve(req.approval_id, "U001")
result = manager.approve(req.approval_id, "U002")
assert result is None
def test_reject(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
updated = manager.reject(req.approval_id)
assert updated is not None
assert updated.status == ApprovalStatus.REJECTED
def test_reject_not_found(self, manager):
assert manager.reject("nonexistent") is None
def test_mark_executed(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
manager.approve(req.approval_id)
updated = manager.mark_executed(req.approval_id, "Execution successful")
assert updated is not None
assert updated.status == ApprovalStatus.EXECUTED
assert updated.exec_result == "Execution successful"
def test_mark_executed_not_approved(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
result = manager.mark_executed(req.approval_id)
assert result is None
def test_mark_failed(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
manager.approve(req.approval_id)
updated = manager.mark_failed(req.approval_id, "Something went wrong")
assert updated is not None
assert updated.status == ApprovalStatus.FAILED
assert updated.exec_error == "Something went wrong"
def test_mark_failed_not_approved(self, manager):
req = manager.create_approval("T", "D", "/c", "C1", "ts")
result = manager.mark_failed(req.approval_id)
assert result is None
@pytest.mark.asyncio
async def test_execute_approval_success(self, manager):
async def handler(**kwargs):
return {"success": True}
manager.register_exec_handler("/cmd", handler)
req = manager.create_approval("T", "D", "/cmd arg1", "C1", "ts")
manager.approve(req.approval_id)
result = await manager.execute_approval(req.approval_id)
assert result["success"] is True
@pytest.mark.asyncio
async def test_execute_approval_no_handler(self, manager):
req = manager.create_approval("T", "D", "/unknown_cmd", "C1", "ts")
manager.approve(req.approval_id)
result = await manager.execute_approval(req.approval_id)
assert result["success"] is False
assert "No exec handler" in result["error"]
@pytest.mark.asyncio
async def test_execute_approval_not_approved(self, manager):
req = manager.create_approval("T", "D", "/cmd", "C1", "ts")
result = await manager.execute_approval(req.approval_id)
assert result["success"] is False
@pytest.mark.asyncio
async def test_execute_approval_handler_error(self, manager):
async def failing_handler(**kwargs):
raise ValueError("boom")
manager.register_exec_handler("/cmd", failing_handler)
req = manager.create_approval("T", "D", "/cmd", "C1", "ts")
manager.approve(req.approval_id)
result = await manager.execute_approval(req.approval_id)
assert result["success"] is False
assert "boom" in result["error"]
def test_list_pending_all(self, manager):
manager.create_approval("T1", "D1", "/c1", "C1", "ts")
manager.create_approval("T2", "D2", "/c2", "C1", "ts")
pending = manager.list_pending()
assert len(pending) == 2
def test_list_pending_by_chat(self, manager):
manager.create_approval("T1", "D1", "/c1", "C1", "ts")
manager.create_approval("T2", "D2", "/c2", "C2", "ts")
pending = manager.list_pending(chat_id="C1")
assert len(pending) == 1
assert pending[0]["title"] == "T1"
def test_list_pending_excludes_resolved(self, manager):
req = manager.create_approval("T1", "D1", "/c1", "C1", "ts")
manager.approve(req.approval_id)
pending = manager.list_pending()
assert len(pending) == 0
def test_cleanup_expired_resolved(self, manager):
manager._ttl_seconds = 0.01
req = manager.create_approval("T", "D", "/c", "C1", "ts")
req.created_at = datetime.now(UTC) - timedelta(seconds=10)
manager.reject(req.approval_id)
manager.create_approval("T2", "D2", "/c2", "C2", "ts")
manager._cleanup_expired()
assert manager.get_approval(req.approval_id) is None
@pytest.mark.asyncio
async def test_execute_approval_not_found(self, manager):
result = await manager.execute_approval("nonexistent")
assert result["success"] is False
def test_register_exec_handler_prefix_match(self, manager):
async def handler(**kwargs):
return {"text": kwargs["command"]}
manager.register_exec_handler("/cmd", handler)
req = manager.create_approval("T", "D", "/cmd arg1", "C1", "ts")
manager.approve(req.approval_id)
import asyncio
result = asyncio.get_event_loop().run_until_complete(manager.execute_approval(req.approval_id))
assert result["success"] is True
class TestNormalizeApproverId:
def test_slack_format(self):
assert normalize_approver_id("slack:U12345") == "U12345"
def test_mention_format(self):
assert normalize_approver_id("<@U12345>") == "U12345"
def test_plain_id(self):
assert normalize_approver_id("U12345") == "U12345"
def test_strips_whitespace(self):
assert normalize_approver_id(" U12345 ") == "U12345"
def test_unknown_format_passthrough(self):
assert normalize_approver_id("unknown_id") == "unknown_id"
class TestExecApprovalConfig:
def test_defaults(self):
config = ExecApprovalConfig()
assert config.enabled == "auto"
assert config.approvers == []
assert config.normalized_approvers == []
def test_with_approvers(self):
config = ExecApprovalConfig(approvers=["slack:U001", "U002", "<@U003>"])
assert config.normalized_approvers == ["U001", "U002", "U003"]
def test_is_approver_in_list(self):
config = ExecApprovalConfig(approvers=["U001", "U002"])
assert config.is_approver("U001") is True
assert config.is_approver("U003") is False
def test_is_approver_empty_list(self):
config = ExecApprovalConfig(approvers=[])
assert config.is_approver("U001") is True
def test_is_approver_with_format(self):
config = ExecApprovalConfig(approvers=["U001"])
assert config.is_approver("<@U001>") is True
assert config.is_approver("slack:U001") is True
def test_from_config_none(self):
config = ExecApprovalConfig.from_config(None)
assert config.enabled == "auto"
assert config.approvers == []
def test_from_config_empty(self):
config = ExecApprovalConfig.from_config({})
assert config.enabled == "auto"
assert config.approvers == []
def test_from_config_with_values(self):
config = ExecApprovalConfig.from_config(
{
"execApprovals": {
"enabled": "manual",
"approvers": ["U001", "U002"],
}
}
)
assert config.enabled == "manual"
assert config.approvers == ["U001", "U002"]
class TestResolveOriginTarget:
def test_turn_source(self):
assert resolve_origin_target("turn", "session", "fallback") == "turn"
def test_session_target(self):
assert resolve_origin_target("", "session", "fallback") == "session"
def test_fallback_target(self):
assert resolve_origin_target("", "", "fallback") == "fallback"
def test_all_empty(self):
assert resolve_origin_target("", "", "") == ""
class TestIsAuthorizedSender:
def test_authorized(self):
config = ExecApprovalConfig(approvers=["U001"])
assert is_authorized_sender("U001", config) is True
def test_not_authorized(self):
config = ExecApprovalConfig(approvers=["U001"])
assert is_authorized_sender("U002", config) is False
def test_empty_approvers_authorizes_all(self):
config = ExecApprovalConfig(approvers=[])
assert is_authorized_sender("U001", config) is True