237 lines
8.9 KiB
Python
237 lines
8.9 KiB
Python
|
|
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"]
|