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"]