Refactor/cli command manager (#178)
* feat(cli): migrate command handling to CommandManager and enhance UI interactions * feat(cli): add /clear and /help commands to enhance user experience * feat(cli): implement /new and /resume commands with interactive session management * Refactor MCP and Skills Command Handling - Moved the interactive picker style to a centralized widget for consistency across MCP and Skills commands. - Updated the MCP command to remove the old command dispatch logic, delegating to the new InstallMCPCommand. - Enhanced the Skills command to utilize a new interactive picker for skill selection, improving user experience. - Implemented cancellation handling in the picker to differentiate between user cancellations and empty selections. - Added comprehensive tests for the new command structures and picker functionalities to ensure reliability. * refactor(cli): streamline CommandManager dispatch and remove deprecated command set * refactor(cli): enhance error handling and state management in ChannelCommand and RichCLICommandUI * refactor(cli): update lifecycle callback terminology and improve async prompt handling in RichCLICommandUI * refactor(cli): enhance SlashCommandCompleter to dynamically fetch workspace directory for autocompletion * refactor(cli): unify quit handling in RichCLICommandUI with shared _stop helper * refactor(cli): remove hardcoded slash commands and utilize command manager for dynamic completion * refactor(cli): update MCP and skills command files for improved clarity and organization * refactor(mcp_ui): remove unnecessary newline in _show_mcp_config function
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Tests for the /threads command."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from rich.table import Table
|
||||
|
||||
from tests.conftest import run_async as _run
|
||||
|
||||
|
||||
def _ctx(**overrides):
|
||||
from EvoScientist.commands.base import CommandContext
|
||||
|
||||
ui = MagicMock()
|
||||
ui.supports_interactive = overrides.pop("supports_interactive", True)
|
||||
return CommandContext(
|
||||
agent=None,
|
||||
thread_id=overrides.pop("thread_id", "tid-1"),
|
||||
ui=ui,
|
||||
workspace_dir=overrides.pop("workspace_dir", "/ws"),
|
||||
), ui
|
||||
|
||||
|
||||
class TestThreadsCommand:
|
||||
def test_empty_list_prints_message(self):
|
||||
from EvoScientist.commands.implementation.session import ThreadsCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
with patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=[]),
|
||||
):
|
||||
_run(ThreadsCommand().execute(ctx, []))
|
||||
ui.append_system.assert_called_once()
|
||||
assert "No saved sessions" in ui.append_system.call_args.args[0]
|
||||
|
||||
def test_renders_table_with_current_marker(self):
|
||||
from EvoScientist.commands.implementation.session import ThreadsCommand
|
||||
|
||||
ctx, ui = _ctx(thread_id="current")
|
||||
threads = [
|
||||
{
|
||||
"thread_id": "current",
|
||||
"preview": "foo",
|
||||
"message_count": 5,
|
||||
"model": "claude",
|
||||
"updated_at": None,
|
||||
},
|
||||
{
|
||||
"thread_id": "other",
|
||||
"preview": "bar",
|
||||
"message_count": 2,
|
||||
"model": None,
|
||||
"updated_at": None,
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=threads),
|
||||
):
|
||||
_run(ThreadsCommand().execute(ctx, []))
|
||||
ui.mount_renderable.assert_called_once()
|
||||
table = ui.mount_renderable.call_args.args[0]
|
||||
assert isinstance(table, Table)
|
||||
# Footer hint (ported from the pre-migration inline /threads handler)
|
||||
footer = ui.append_system.call_args.args[0]
|
||||
assert "/resume" in footer
|
||||
assert "/delete" in footer
|
||||
assert "/new" in footer
|
||||
|
||||
def test_footer_hint_suppressed_in_channel_mode(self):
|
||||
"""Channels don't get the footer — keeps outbound text short."""
|
||||
from EvoScientist.commands.implementation.session import ThreadsCommand
|
||||
|
||||
ctx, ui = _ctx(supports_interactive=False)
|
||||
threads = [
|
||||
{
|
||||
"thread_id": "t",
|
||||
"preview": "p",
|
||||
"message_count": 1,
|
||||
"model": "m",
|
||||
"updated_at": None,
|
||||
}
|
||||
]
|
||||
with patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=threads),
|
||||
):
|
||||
_run(ThreadsCommand().execute(ctx, []))
|
||||
ui.append_system.assert_not_called()
|
||||
|
||||
def test_channel_mode_drops_model_column(self):
|
||||
"""Non-interactive (channel) UIs get a narrower table."""
|
||||
from EvoScientist.commands.implementation.session import ThreadsCommand
|
||||
|
||||
ctx, ui = _ctx(supports_interactive=False)
|
||||
threads = [
|
||||
{
|
||||
"thread_id": "t",
|
||||
"preview": "p",
|
||||
"message_count": 1,
|
||||
"model": "m",
|
||||
"updated_at": None,
|
||||
}
|
||||
]
|
||||
with patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=threads),
|
||||
):
|
||||
_run(ThreadsCommand().execute(ctx, []))
|
||||
# Channel mode: no Model column. 4 columns: ID, Preview, Msgs, Last Used.
|
||||
table = ui.mount_renderable.call_args.args[0]
|
||||
column_headers = [col.header for col in table.columns]
|
||||
assert "Model" not in column_headers
|
||||
Reference in New Issue
Block a user