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,141 @@
|
||||
"""Tests for the /delete command."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from tests.conftest import run_async as _run
|
||||
|
||||
|
||||
def _ctx(thread_id="current"):
|
||||
from EvoScientist.commands.base import CommandContext
|
||||
|
||||
ui = MagicMock()
|
||||
ui.supports_interactive = True
|
||||
return CommandContext(agent=None, thread_id=thread_id, ui=ui), ui
|
||||
|
||||
|
||||
def _patches(thread_exists=False, similar=None, deleted=True, threads=None):
|
||||
"""Return a context manager stack patching the sessions module."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
stack = ExitStack()
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"EvoScientist.sessions.thread_exists",
|
||||
new=AsyncMock(return_value=thread_exists),
|
||||
)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"EvoScientist.sessions.find_similar_threads",
|
||||
new=AsyncMock(return_value=similar or []),
|
||||
)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"EvoScientist.sessions.delete_thread",
|
||||
new=AsyncMock(return_value=deleted),
|
||||
)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=threads or []),
|
||||
)
|
||||
)
|
||||
return stack
|
||||
|
||||
|
||||
class TestDeleteCommand:
|
||||
def test_refuses_to_delete_current(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx(thread_id="current")
|
||||
# Inline the patches here (rather than using ``_patches``) so we
|
||||
# can keep a direct handle on the ``delete_thread`` mock and
|
||||
# assert on it *inside* the context. Asserting after the
|
||||
# context exits hits the real function (no ``await_count``
|
||||
# attr), which silently degrades into ``assert True``.
|
||||
mock_delete = AsyncMock(return_value=True)
|
||||
with (
|
||||
patch(
|
||||
"EvoScientist.sessions.thread_exists",
|
||||
new=AsyncMock(return_value=True),
|
||||
),
|
||||
patch("EvoScientist.sessions.delete_thread", new=mock_delete),
|
||||
patch(
|
||||
"EvoScientist.sessions.find_similar_threads",
|
||||
new=AsyncMock(return_value=[]),
|
||||
),
|
||||
patch(
|
||||
"EvoScientist.sessions.list_threads",
|
||||
new=AsyncMock(return_value=[]),
|
||||
),
|
||||
):
|
||||
_run(DeleteCommand().execute(ctx, ["current"]))
|
||||
assert mock_delete.await_count == 0
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("Cannot delete the current session" in m for m in msgs)
|
||||
|
||||
def test_happy_path_success(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx(thread_id="current")
|
||||
with _patches(thread_exists=True, deleted=True):
|
||||
_run(DeleteCommand().execute(ctx, ["other-thread"]))
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("Deleted session other-thread" in m for m in msgs)
|
||||
|
||||
def test_not_found(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
with _patches(thread_exists=False, similar=[]):
|
||||
_run(DeleteCommand().execute(ctx, ["missing"]))
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("not found" in m for m in msgs)
|
||||
|
||||
def test_ambiguous_prefix(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
with _patches(thread_exists=False, similar=["abc-one", "abc-two"]):
|
||||
_run(DeleteCommand().execute(ctx, ["abc"]))
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("Ambiguous" in m for m in msgs)
|
||||
|
||||
def test_prefix_resolves_to_unique_match(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
with _patches(thread_exists=False, similar=["abc-one"], deleted=True):
|
||||
_run(DeleteCommand().execute(ctx, ["abc"]))
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("Deleted session abc-one" in m for m in msgs)
|
||||
|
||||
def test_no_arg_empty_sessions_prints_notice(self):
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
with _patches(threads=[]):
|
||||
_run(DeleteCommand().execute(ctx, []))
|
||||
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
||||
assert any("No sessions to delete" in m for m in msgs)
|
||||
|
||||
def test_no_arg_calls_picker_returns_none(self):
|
||||
"""When no arg and picker returns None, nothing is deleted."""
|
||||
from EvoScientist.commands.implementation.session import DeleteCommand
|
||||
|
||||
ctx, ui = _ctx()
|
||||
ui.wait_for_thread_pick = AsyncMock(return_value=None)
|
||||
threads = [
|
||||
{
|
||||
"thread_id": "t1",
|
||||
"preview": "",
|
||||
"message_count": 1,
|
||||
"model": None,
|
||||
"updated_at": None,
|
||||
}
|
||||
]
|
||||
with _patches(threads=threads):
|
||||
_run(DeleteCommand().execute(ctx, []))
|
||||
ui.wait_for_thread_pick.assert_awaited_once()
|
||||
Reference in New Issue
Block a user