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:
MoltBot Service
2026-02-13 13:13:38 +00:00
parent a1a6ca9a3f
commit 5bdceff732
6 changed files with 475 additions and 10 deletions

View File

@@ -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()