644 lines
19 KiB
Python
644 lines
19 KiB
Python
"""Wiring tests for the commands package: every handler reaches its service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from discord.ext import commands
|
|
|
|
from vibe_bot.commands.custom_bots import MAX_PERSONALITY_LENGTH
|
|
from vibe_bot.tests._helpers import invoke, sent_texts
|
|
from vibe_bot.tests.conftest import AppMocks
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# custom_bots: create / list / delete
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_custom_bot_success_updates_cache(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A successful create invalidates the cache with the new bot."""
|
|
app_mocks.manager.create_custom_bot.return_value = "created"
|
|
app_mocks.manager.list_custom_bots.return_value = [
|
|
("newbot", "a personality", "user123"),
|
|
]
|
|
|
|
invoke(
|
|
bot, "custom-bot", mock_ctx, "newbot", personality="you are a british butler"
|
|
)
|
|
|
|
app_mocks.manager.create_custom_bot.assert_called_once_with(
|
|
bot_name="newbot",
|
|
system_prompt="you are a british butler",
|
|
created_by="12345",
|
|
)
|
|
assert app_mocks.app.bot_cache == {"newbot": ("a personality", "user123")}
|
|
assert "alfred" not in app_mocks.app.bot_cache
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("has been created" in t for t in texts)
|
|
assert any("You can now use this bot" in t for t in texts)
|
|
|
|
|
|
def test_custom_bot_replaced_message(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""When the name already exists, the command reports a replace, not a create."""
|
|
app_mocks.manager.create_custom_bot.return_value = "replaced"
|
|
|
|
invoke(
|
|
bot, "custom-bot", mock_ctx, "alfred", personality="you are a british butler"
|
|
)
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("already existed" in t and "replaced" in t for t in texts)
|
|
|
|
|
|
def test_custom_bot_invalid_name_too_short(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A one-character name is rejected before any manager call."""
|
|
invoke(bot, "custom-bot", mock_ctx, "a", personality="this is a valid personality")
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Invalid bot name" in t for t in texts)
|
|
app_mocks.manager.create_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_custom_bot_invalid_name_empty(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""An empty name is rejected before any manager call."""
|
|
invoke(bot, "custom-bot", mock_ctx, "", personality="this is a valid personality")
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Invalid bot name" in t for t in texts)
|
|
app_mocks.manager.create_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_custom_bot_invalid_personality(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A personality under 10 characters is rejected."""
|
|
invoke(bot, "custom-bot", mock_ctx, "testbot", personality="short")
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Invalid personality" in t for t in texts)
|
|
app_mocks.manager.create_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_custom_bot_personality_too_long(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A personality over MAX_PERSONALITY_LENGTH is rejected before any DB call."""
|
|
invoke(
|
|
bot,
|
|
"custom-bot",
|
|
mock_ctx,
|
|
"testbot",
|
|
personality="a" * (MAX_PERSONALITY_LENGTH + 1),
|
|
)
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Personality too long" in t for t in texts)
|
|
app_mocks.manager.create_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_custom_bot_create_fails(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A failed create reports an error and does not invalidate the cache."""
|
|
app_mocks.manager.create_custom_bot.return_value = False
|
|
|
|
invoke(
|
|
bot, "custom-bot", mock_ctx, "alfred", personality="you are a british butler"
|
|
)
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Failed to create custom bot" in t for t in texts)
|
|
app_mocks.manager.list_custom_bots.assert_not_called()
|
|
assert app_mocks.app.bot_cache == {"alfred": ("british butler", "user123")}
|
|
|
|
|
|
def test_list_custom_bots_empty(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""Listing with no bots suggests creating one."""
|
|
app_mocks.manager.list_custom_bots.return_value = []
|
|
|
|
invoke(bot, "list-custom-bots", mock_ctx)
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("No custom bots" in t for t in texts)
|
|
|
|
|
|
def test_list_custom_bots_with_bots(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""Listing shows every bot name."""
|
|
app_mocks.manager.list_custom_bots.return_value = [
|
|
("alfred", "british butler", "user-1"),
|
|
("jarvis", "ai assistant", "user-2"),
|
|
]
|
|
|
|
invoke(bot, "list-custom-bots", mock_ctx)
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Available Custom Bots" in t for t in texts)
|
|
assert any("* alfred" in t and "* jarvis" in t for t in texts)
|
|
|
|
|
|
def test_delete_custom_bot_success(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""The creator can delete their bot; the cache is invalidated."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"prompt",
|
|
"12345",
|
|
"2024-01-01",
|
|
)
|
|
app_mocks.manager.delete_custom_bot.return_value = True
|
|
app_mocks.manager.list_custom_bots.return_value = []
|
|
|
|
invoke(bot, "delete-custom-bot", mock_ctx, "alfred")
|
|
|
|
app_mocks.manager.delete_custom_bot.assert_called_once_with("alfred")
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("has been deleted" in t for t in texts)
|
|
assert app_mocks.app.bot_cache == {}
|
|
|
|
|
|
def test_delete_custom_bot_not_found(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""Deleting a non-existent bot reports not found."""
|
|
app_mocks.manager.get_custom_bot.return_value = None
|
|
|
|
invoke(bot, "delete-custom-bot", mock_ctx, "nonexistent")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("not found" in t for t in texts)
|
|
app_mocks.manager.delete_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_delete_custom_bot_not_owner(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A non-owner cannot delete the bot."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"prompt",
|
|
"other-user-id",
|
|
"2024-01-01",
|
|
)
|
|
|
|
invoke(bot, "delete-custom-bot", mock_ctx, "alfred")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("You can only delete your own" in t for t in texts)
|
|
app_mocks.manager.delete_custom_bot.assert_not_called()
|
|
|
|
|
|
def test_delete_custom_bot_delete_fails(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A failed delete reports an error and keeps the cache."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"prompt",
|
|
"12345",
|
|
"2024-01-01",
|
|
)
|
|
app_mocks.manager.delete_custom_bot.return_value = False
|
|
|
|
invoke(bot, "delete-custom-bot", mock_ctx, "alfred")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Failed to delete" in t for t in texts)
|
|
app_mocks.manager.list_custom_bots.assert_not_called()
|
|
assert app_mocks.app.bot_cache == {"alfred": ("british butler", "user123")}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# speech: speak / voices
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_speak_delegates_to_service(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!speak forwards to SpeechService.speak."""
|
|
invoke(bot, "speak", mock_ctx, message="hello world")
|
|
|
|
app_mocks.speech.speak.assert_awaited_once_with(mock_ctx, message="hello world")
|
|
|
|
|
|
def test_speak_cooldown_blocks_third_invocation(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""speak allows 3 per 30s; the 4th immediate call is rate-limited."""
|
|
cmd = bot.get_command("speak")
|
|
assert cmd is not None
|
|
assert cmd.cooldown is not None
|
|
|
|
mock_ctx.message.edited_at = None
|
|
mock_ctx.message.created_at = datetime(2026, 1, 1, tzinfo=UTC)
|
|
|
|
for _ in range(3):
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
with pytest.raises(commands.CommandOnCooldown) as exc_info:
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
|
|
asyncio.run(bot.on_command_error(mock_ctx, exc_info.value))
|
|
|
|
app_mocks.speech.speak.assert_not_awaited()
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("too quickly" in t for t in texts)
|
|
|
|
|
|
def test_voices(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!voices lists the voice catalog."""
|
|
invoke(bot, "voices", mock_ctx)
|
|
|
|
full = "\n".join(sent_texts(mock_ctx))
|
|
assert "Available Voices" in full
|
|
assert "af_sarah" in full
|
|
assert "Use `!speak" in full
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# images: doodlebob / retcon
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_doodlebob_delegates_to_service(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!doodlebob forwards to ImageService.generate."""
|
|
invoke(bot, "doodlebob", mock_ctx, message="a centaur")
|
|
|
|
app_mocks.image.generate.assert_awaited_once_with(mock_ctx, message="a centaur")
|
|
|
|
|
|
def test_retcon_delegates_to_service(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!retcon forwards to ImageService.edit."""
|
|
invoke(bot, "retcon", mock_ctx, message="make it blue")
|
|
|
|
app_mocks.image.edit.assert_awaited_once_with(mock_ctx, message="make it blue")
|
|
|
|
|
|
def test_doodlebob_cooldown_blocks_second_invocation(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A second immediate doodlebob invocation is rate-limited before the LLM."""
|
|
cmd = bot.get_command("doodlebob")
|
|
assert cmd is not None
|
|
assert cmd.cooldown is not None
|
|
|
|
mock_ctx.message.edited_at = None
|
|
mock_ctx.message.created_at = datetime(2026, 1, 1, tzinfo=UTC)
|
|
|
|
# First invocation consumes the single token (no error).
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
# Second immediate invocation is rejected before the service is called.
|
|
with pytest.raises(commands.CommandOnCooldown) as exc_info:
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
|
|
# The error handler turns it into a friendly message.
|
|
asyncio.run(bot.on_command_error(mock_ctx, exc_info.value))
|
|
|
|
app_mocks.image.generate.assert_not_awaited()
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("too quickly" in t for t in texts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# conversation: talkforme
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_talkforme_delegates_to_service(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!talkforme parses its args, then forwards to ConversationService.run."""
|
|
invoke(bot, "talkforme", mock_ctx, message="a b 3 talking cats")
|
|
|
|
app_mocks.conversation.run.assert_awaited_once_with(
|
|
mock_ctx, "a", "b", "3", "talking cats"
|
|
)
|
|
|
|
|
|
def test_talkforme_invalid_args(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!talkforme with too few parts shows usage and calls no service."""
|
|
invoke(bot, "talkforme", mock_ctx, message="bot1 bot2")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("Usage" in t for t in texts)
|
|
app_mocks.conversation.run.assert_not_awaited()
|
|
|
|
|
|
def test_talkforme_cooldown_blocks_second_invocation(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""A second immediate talkforme invocation is rate-limited before the LLM."""
|
|
cmd = bot.get_command("talkforme")
|
|
assert cmd is not None
|
|
assert cmd.cooldown is not None
|
|
|
|
mock_ctx.message.edited_at = None
|
|
mock_ctx.message.created_at = datetime(2026, 1, 1, tzinfo=UTC)
|
|
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
with pytest.raises(commands.CommandOnCooldown) as exc_info:
|
|
cmd._prepare_cooldowns(mock_ctx)
|
|
|
|
asyncio.run(bot.on_command_error(mock_ctx, exc_info.value))
|
|
|
|
app_mocks.conversation.run.assert_not_awaited()
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("too quickly" in t for t in texts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# admin: lobotomize / debug / history
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_lobotomize(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!lobotomize clears all messages on the shared db."""
|
|
invoke(bot, "lobotomize", mock_ctx)
|
|
|
|
app_mocks.db.clear_all_messages.assert_called_once_with()
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("cleared" in t for t in texts)
|
|
|
|
|
|
def test_history_bot_not_found(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!history on an unknown bot reports not found."""
|
|
app_mocks.manager.get_custom_bot.return_value = None
|
|
|
|
invoke(bot, "history", mock_ctx, "nonexistent")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("not found" in t for t in texts)
|
|
app_mocks.db.get_bot_history.assert_not_called()
|
|
|
|
|
|
def test_history_no_history(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!history on a bot with no messages says so."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"british butler",
|
|
"user-123",
|
|
"2024-01-01",
|
|
)
|
|
app_mocks.db.get_bot_history.return_value = []
|
|
|
|
invoke(bot, "history", mock_ctx, "alfred")
|
|
|
|
texts = sent_texts(mock_ctx)
|
|
assert any("No chat history" in t and "**alfred**" in t for t in texts)
|
|
|
|
|
|
def test_history_with_data(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!history formats the stored exchange, newest last."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"british butler",
|
|
"user-123",
|
|
"2024-01-01",
|
|
)
|
|
app_mocks.db.get_bot_history.return_value = [
|
|
("hello", "yes master?"),
|
|
("what time is it", "it is currently 3pm"),
|
|
]
|
|
|
|
invoke(bot, "history", mock_ctx, "alfred")
|
|
|
|
full = "\n".join(sent_texts(mock_ctx))
|
|
assert "Chat History for **alfred**" in full
|
|
assert "what time is it" in full
|
|
assert "alfred: it is currently 3pm" in full
|
|
|
|
|
|
def test_history_long_response_chunked(
|
|
app_mocks: AppMocks,
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""Long history payloads are split into multiple sends."""
|
|
app_mocks.manager.get_custom_bot.return_value = (
|
|
"alfred",
|
|
"british butler",
|
|
"user-123",
|
|
"2024-01-01",
|
|
)
|
|
app_mocks.db.get_bot_history.return_value = [
|
|
("x" * 2000, "y" * 2000),
|
|
]
|
|
|
|
invoke(bot, "history", mock_ctx, "alfred")
|
|
|
|
assert mock_ctx.send.call_count >= 2
|
|
|
|
|
|
def test_debug_no_subcommand(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug without a subcommand shows the menu."""
|
|
invoke(bot, "debug", mock_ctx, subcommand=None)
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "Debug Menu" in call_args
|
|
assert "members" in call_args
|
|
|
|
|
|
def test_debug_members_no_guild(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug members on a channel without guild members says so."""
|
|
mock_ctx.channel.guild = None
|
|
|
|
invoke(bot, "debug", mock_ctx, subcommand="members")
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "No members found in this channel." in call_args
|
|
|
|
|
|
def test_debug_members_with_members(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug members lists the guild members."""
|
|
mock_member = MagicMock()
|
|
mock_member.display_name = "Alice"
|
|
mock_member.name = "alice"
|
|
mock_member.nick = None
|
|
mock_member.global_name = None
|
|
mock_member.status = MagicMock(value="online")
|
|
mock_ctx.channel.guild.members = [mock_member]
|
|
|
|
invoke(bot, "debug", mock_ctx, subcommand="members")
|
|
|
|
assert mock_ctx.send.called
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "Alice" in call_args
|
|
assert "1 total" in call_args
|
|
|
|
|
|
def test_debug_unknown_subcommand(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug with an unknown subcommand explains the usage."""
|
|
invoke(bot, "debug", mock_ctx, subcommand="unknown")
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "Unknown debug sub-command" in call_args
|
|
|
|
|
|
def test_debug_members_many_chunks(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug members with many members exceeds the chunk limit."""
|
|
mock_members = []
|
|
for i in range(50):
|
|
mock_member = MagicMock()
|
|
mock_member.display_name = f"User{i}_with_a_very_long_display_name"
|
|
mock_member.name = f"user{i}"
|
|
mock_member.nick = None
|
|
mock_member.global_name = None
|
|
mock_member.status = MagicMock(value="online")
|
|
mock_members.append(mock_member)
|
|
mock_ctx.channel.guild.members = mock_members
|
|
|
|
invoke(bot, "debug", mock_ctx, subcommand="members")
|
|
|
|
assert mock_ctx.send.call_count >= 2
|
|
first_chunk = mock_ctx.send.call_args_list[0][0][0]
|
|
assert "Members in this channel (50 total)" in first_chunk
|
|
|
|
|
|
def test_debug_whoami(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug whoami shows user info."""
|
|
invoke(bot, "debug", mock_ctx, subcommand="whoami")
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "Username: testuser" in call_args
|
|
assert "User ID: 12345" in call_args
|
|
assert "Global Name: Test User" in call_args
|
|
assert "Nickname: tester" in call_args
|
|
|
|
|
|
def test_debug_whoami_minimal(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug whoami omits fields the user does not have."""
|
|
mock_ctx.author.global_name = None
|
|
mock_ctx.author.nick = None
|
|
mock_ctx.author.top_role.name = "@everyone"
|
|
mock_ctx.author.activities = []
|
|
mock_ctx.author.joined_at = None
|
|
mock_ctx.author.created_at = None
|
|
|
|
invoke(bot, "debug", mock_ctx, subcommand="whoami")
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "Username: testuser" in call_args
|
|
assert "User ID: 12345" in call_args
|
|
assert "Global Name" not in call_args
|
|
assert "Nickname" not in call_args
|
|
assert "Activities" not in call_args
|
|
assert "Joined" not in call_args
|
|
|
|
|
|
def test_debug_tools(
|
|
bot: commands.Bot,
|
|
mock_ctx: MagicMock,
|
|
) -> None:
|
|
"""!debug tools shows the LLM tool schema."""
|
|
invoke(bot, "debug", mock_ctx, subcommand="tools")
|
|
|
|
mock_ctx.send.assert_called_once()
|
|
call_args = mock_ctx.send.call_args[0][0]
|
|
assert "LLM Tools" in call_args
|
|
assert "get_channel_members" in call_args
|
|
assert "members" in call_args.lower()
|