stage-6: model selection and advanced commands
/model (show/change), /restart (owner), /logs, set_session_model API, model reset on /clear. 20 new tests (161 total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import signal
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -500,3 +501,158 @@ class TestStatusSlashCommand:
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "no active session" in msg.args[0].lower()
|
||||
|
||||
|
||||
# --- /clear mentions model reset ---
|
||||
|
||||
|
||||
class TestClearMentionsModelReset:
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.adapters.discord_bot.clear_session")
|
||||
async def test_clear_mentions_model_reset(self, mock_clear, owned_bot):
|
||||
mock_clear.return_value = True
|
||||
cmd = _find_command(owned_bot.tree, "clear")
|
||||
interaction = _mock_interaction(channel_id="900")
|
||||
await cmd.callback(interaction)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
text = msg.args[0]
|
||||
assert "model reset" in text.lower()
|
||||
assert "sonnet" in text.lower()
|
||||
|
||||
|
||||
# --- /model slash command ---
|
||||
|
||||
|
||||
class TestModelSlashCommand:
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.adapters.discord_bot.get_active_session")
|
||||
async def test_model_no_args_shows_current_with_session(self, mock_get, owned_bot):
|
||||
mock_get.return_value = {"model": "opus"}
|
||||
cmd = _find_command(owned_bot.tree, "model")
|
||||
interaction = _mock_interaction(channel_id="900")
|
||||
await cmd.callback(interaction, choice=None)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
text = msg.args[0]
|
||||
assert "opus" in text.lower()
|
||||
assert "haiku" in text
|
||||
assert "sonnet" in text
|
||||
assert msg.kwargs.get("ephemeral") is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.adapters.discord_bot.get_active_session")
|
||||
async def test_model_no_args_shows_default_without_session(self, mock_get, owned_bot):
|
||||
mock_get.return_value = None
|
||||
cmd = _find_command(owned_bot.tree, "model")
|
||||
interaction = _mock_interaction(channel_id="900")
|
||||
await cmd.callback(interaction, choice=None)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
text = msg.args[0]
|
||||
assert "sonnet" in text # default from config
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.adapters.discord_bot.set_session_model")
|
||||
@patch("src.adapters.discord_bot.get_active_session")
|
||||
async def test_model_with_choice_changes_existing_session(self, mock_get, mock_set, owned_bot):
|
||||
mock_get.return_value = {"model": "sonnet", "session_id": "abc"}
|
||||
cmd = _find_command(owned_bot.tree, "model")
|
||||
interaction = _mock_interaction(channel_id="900")
|
||||
choice = MagicMock()
|
||||
choice.value = "opus"
|
||||
await cmd.callback(interaction, choice=choice)
|
||||
|
||||
mock_set.assert_called_once_with("900", "opus")
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "opus" in msg.args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.claude_session._save_sessions")
|
||||
@patch("src.claude_session._load_sessions")
|
||||
@patch("src.adapters.discord_bot.get_active_session")
|
||||
async def test_model_with_choice_presets_when_no_session(self, mock_get, mock_load, mock_save, owned_bot):
|
||||
mock_get.return_value = None
|
||||
mock_load.return_value = {}
|
||||
cmd = _find_command(owned_bot.tree, "model")
|
||||
interaction = _mock_interaction(channel_id="900")
|
||||
choice = MagicMock()
|
||||
choice.value = "haiku"
|
||||
await cmd.callback(interaction, choice=choice)
|
||||
|
||||
mock_save.assert_called_once()
|
||||
saved_data = mock_save.call_args[0][0]
|
||||
assert saved_data["900"]["model"] == "haiku"
|
||||
assert saved_data["900"]["session_id"] == ""
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "haiku" in msg.args[0]
|
||||
|
||||
|
||||
# --- /restart slash command ---
|
||||
|
||||
|
||||
class TestRestartSlashCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_owner_succeeds(self, owned_bot, tmp_path):
|
||||
pid_file = tmp_path / "echo-core.pid"
|
||||
pid_file.write_text("12345")
|
||||
with patch.object(discord_bot, "PROJECT_ROOT", tmp_path), \
|
||||
patch("os.kill") as mock_kill:
|
||||
cmd = _find_command(owned_bot.tree, "restart")
|
||||
interaction = _mock_interaction(user_id="111")
|
||||
await cmd.callback(interaction)
|
||||
|
||||
mock_kill.assert_called_once_with(12345, signal.SIGTERM)
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "restarting" in msg.args[0].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_non_owner_rejected(self, owned_bot):
|
||||
cmd = _find_command(owned_bot.tree, "restart")
|
||||
interaction = _mock_interaction(user_id="999")
|
||||
await cmd.callback(interaction)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "owner only" in msg.args[0].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_no_pid_file(self, owned_bot, tmp_path):
|
||||
with patch.object(discord_bot, "PROJECT_ROOT", tmp_path):
|
||||
cmd = _find_command(owned_bot.tree, "restart")
|
||||
interaction = _mock_interaction(user_id="111")
|
||||
await cmd.callback(interaction)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "no pid file" in msg.args[0].lower()
|
||||
|
||||
|
||||
# --- /logs slash command ---
|
||||
|
||||
|
||||
class TestLogsSlashCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_returns_code_block(self, owned_bot, tmp_path):
|
||||
log_dir = tmp_path / "logs"
|
||||
log_dir.mkdir()
|
||||
log_file = log_dir / "echo-core.log"
|
||||
log_file.write_text("line1\nline2\nline3\n")
|
||||
with patch.object(discord_bot, "PROJECT_ROOT", tmp_path):
|
||||
cmd = _find_command(owned_bot.tree, "logs")
|
||||
interaction = _mock_interaction()
|
||||
await cmd.callback(interaction, n=10)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
text = msg.args[0]
|
||||
assert "```" in text
|
||||
assert "line1" in text
|
||||
assert "line3" in text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_no_file(self, owned_bot, tmp_path):
|
||||
with patch.object(discord_bot, "PROJECT_ROOT", tmp_path):
|
||||
cmd = _find_command(owned_bot.tree, "logs")
|
||||
interaction = _mock_interaction()
|
||||
await cmd.callback(interaction, n=10)
|
||||
|
||||
msg = interaction.response.send_message.call_args
|
||||
assert "no log file" in msg.args[0].lower()
|
||||
|
||||
Reference in New Issue
Block a user