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

237 lines
8.9 KiB
Python
Raw Normal View History

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from slack_sdk.errors import SlackApiError
from yuxi.channels.adapters.slack.commands import (
CommandTier,
SlackCommandRegistry,
SlashCommand,
)
class TestCommandTier:
def test_values(self):
assert CommandTier.PLATFORM == "platform"
assert CommandTier.PLUGIN == "plugin"
assert CommandTier.SKILL == "skill"
assert CommandTier.CUSTOM == "custom"
class TestSlashCommand:
def test_creation_defaults(self):
cmd = SlashCommand(command="/test", description="A test command")
assert cmd.command == "/test"
assert cmd.description == "A test command"
assert cmd.usage_hint == ""
assert cmd.tier == CommandTier.PLATFORM
assert cmd.handler is None
def test_creation_with_all_fields(self):
async def handler(**kwargs):
return {"text": "ok"}
cmd = SlashCommand(
command="/custom",
description="Custom command",
usage_hint="[args]",
tier=CommandTier.CUSTOM,
handler=handler,
)
assert cmd.command == "/custom"
assert cmd.usage_hint == "[args]"
assert cmd.tier == CommandTier.CUSTOM
assert cmd.handler is handler
def test_to_slack_definition(self):
cmd = SlashCommand(
command="/cmd",
description="desc",
usage_hint="[args]",
)
definition = cmd.to_slack_definition()
assert definition["command"] == "/cmd"
assert definition["description"] == "desc"
assert definition["usage_hint"] == "[args]"
class TestSlackCommandRegistry:
@pytest.fixture
def registry(self):
return SlackCommandRegistry()
def test_register_command(self, registry):
cmd = SlashCommand(command="/test", description="Test")
registry.register(cmd)
assert registry.get_command("/test") is cmd
def test_get_command_not_found(self, registry):
assert registry.get_command("/nonexistent") is None
def test_register_defaults(self, registry):
registry.register_defaults()
assert registry.get_command("/fp-agentstatus") is not None
assert registry.get_command("/fp-status") is not None
assert registry.get_command("/fp-help") is not None
assert registry.get_command("/fp-dm-policy") is not None
def test_all_commands(self, registry):
registry.register_defaults()
all_cmds = registry.all_commands
assert len(all_cmds) == 4
def test_commands_property_returns_copy(self, registry):
registry.register_defaults()
cmds = registry.commands
cmds["/new"] = None
assert "/new" not in registry.commands
def test_platform_commands(self, registry):
cmd = SlashCommand(command="/test", description="Test", tier=CommandTier.PLATFORM)
registry.register(cmd)
assert len(registry.platform_commands) == 1
def test_plugin_commands(self, registry):
cmd = SlashCommand(command="/pcmd", description="Plugin", tier=CommandTier.PLUGIN)
registry.register(cmd)
assert len(registry.plugin_commands) == 1
def test_skill_commands(self, registry):
cmd = SlashCommand(command="/scmd", description="Skill", tier=CommandTier.SKILL)
registry.register(cmd)
assert len(registry.skill_commands) == 1
def test_custom_commands(self, registry):
cmd = SlashCommand(command="/ccmd", description="Custom", tier=CommandTier.CUSTOM)
registry.register(cmd)
assert len(registry.custom_commands) == 1
def test_get_tier_commands_empty_tier(self, registry):
assert registry.get_tier_commands(CommandTier.PLUGIN) == []
def test_multiple_commands_same_tier(self, registry):
registry.register(SlashCommand(command="/a", description="A", tier=CommandTier.PLATFORM))
registry.register(SlashCommand(command="/b", description="B", tier=CommandTier.PLATFORM))
assert len(registry.platform_commands) == 2
@pytest.mark.asyncio
async def test_handle_unknown_command(self, registry):
result = await registry.handle_command("/unknown", "", "U001", "C001")
assert "未知命令" in result["text"]
assert result["response_type"] == "ephemeral"
@pytest.mark.asyncio
async def test_handle_agentstatus(self, registry):
registry.register_defaults()
result = await registry.handle_command("/fp-agentstatus", "", "U001", "C001")
assert "Bot 运行中" in result["text"]
assert result["response_type"] == "ephemeral"
@pytest.mark.asyncio
async def test_handle_status_alias(self, registry):
registry.register_defaults()
result = await registry.handle_command("/fp-status", "", "U001", "C001")
assert "Bot 运行中" in result["text"]
@pytest.mark.asyncio
async def test_handle_help(self, registry):
registry.register_defaults()
result = await registry.handle_command("/fp-help", "", "U001", "C001")
assert "ForcePilot" in result["text"]
assert "平台命令" in result["text"]
@pytest.mark.asyncio
async def test_handle_dm_policy_no_args(self, registry):
registry.register_defaults()
result = await registry.handle_command("/fp-dm-policy", "", "U001", "C001")
assert "未指定" in result["text"]
@pytest.mark.asyncio
async def test_handle_dm_policy_with_args(self, registry):
registry.register_defaults()
result = await registry.handle_command("/fp-dm-policy", "open", "U001", "C001")
assert "open" in result["text"]
@pytest.mark.asyncio
async def test_handle_custom_handler(self, registry):
async def custom_handler(**kwargs):
return {"text": f"Got: {kwargs['text']}"}
cmd = SlashCommand(
command="/custom",
description="Custom",
handler=custom_handler,
)
registry.register(cmd)
result = await registry.handle_command("/custom", "hello", "U001", "C001")
assert result["text"] == "Got: hello"
@pytest.mark.asyncio
async def test_handle_custom_handler_error(self, registry):
async def failing_handler(**kwargs):
raise RuntimeError("boom")
cmd = SlashCommand(
command="/fail",
description="Fails",
handler=failing_handler,
)
registry.register(cmd)
result = await registry.handle_command("/fail", "", "U001", "C001")
assert "命令执行失败" in result["text"]
@pytest.mark.asyncio
async def test_sync_to_slack_success(self, registry):
mock_client = AsyncMock()
mock_client.api_call = AsyncMock(return_value={"ok": True})
registry.register(SlashCommand(command="/test", description="Test"))
results = await registry.sync_to_slack(mock_client)
assert results["/test"] is True
@pytest.mark.asyncio
async def test_sync_to_slack_name_taken(self, registry):
mock_client = AsyncMock()
mock_response = {"ok": False, "error": "name_taken"}
mock_client.api_call = AsyncMock(side_effect=SlackApiError("name_taken", mock_response))
registry.register(SlashCommand(command="/test", description="Test"))
results = await registry.sync_to_slack(mock_client)
assert results["/test"] is True
@pytest.mark.asyncio
async def test_sync_to_slack_already_exists(self, registry):
mock_client = AsyncMock()
mock_response = {"ok": False, "error": "already_exists"}
mock_client.api_call = AsyncMock(side_effect=SlackApiError("already_exists", mock_response))
registry.register(SlashCommand(command="/test", description="Test"))
results = await registry.sync_to_slack(mock_client)
assert results["/test"] is True
@pytest.mark.asyncio
async def test_sync_to_slack_failure(self, registry):
mock_client = AsyncMock()
mock_response = {"ok": False, "error": "missing_scope"}
mock_client.api_call = AsyncMock(side_effect=SlackApiError("missing_scope", mock_response))
registry.register(SlashCommand(command="/test", description="Test"))
results = await registry.sync_to_slack(mock_client)
assert results["/test"] is False
@pytest.mark.asyncio
async def test_sync_to_slack_generic_exception(self, registry):
mock_client = AsyncMock()
mock_client.api_call = AsyncMock(side_effect=Exception("network error"))
registry.register(SlashCommand(command="/test", description="Test"))
results = await registry.sync_to_slack(mock_client)
assert results["/test"] is False
@pytest.mark.asyncio
async def test_handle_unregistered_with_handler(self, registry):
result = await registry.handle_command("/nonexistent", "", "U001", "C001")
assert result["response_type"] == "ephemeral"
assert "未知命令" in result["text"]