mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-17 01:26:40 +03:00
refactor(cli): split commands into focused modules (#5175)
This commit is contained in:
+28
-28
@@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
import pytest
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
|
||||
from nanobot.cli import commands
|
||||
from nanobot.cli import stream as stream_mod
|
||||
from nanobot.cli import terminal
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -14,8 +14,8 @@ def mock_prompt_session():
|
||||
"""Mock the global prompt session."""
|
||||
mock_session = MagicMock()
|
||||
mock_session.prompt_async = AsyncMock()
|
||||
with patch("nanobot.cli.commands._PROMPT_SESSION", mock_session), \
|
||||
patch("nanobot.cli.commands.patch_stdout"):
|
||||
with patch("nanobot.cli.terminal._prompt_session", mock_session), \
|
||||
patch("nanobot.cli.terminal.patch_stdout"):
|
||||
yield mock_session
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ async def test_read_interactive_input_async_returns_input(mock_prompt_session):
|
||||
"""Test that _read_interactive_input_async returns the user input from prompt_session."""
|
||||
mock_prompt_session.prompt_async.return_value = "hello world"
|
||||
|
||||
result = await commands._read_interactive_input_async()
|
||||
result = await terminal._read_interactive_input_async()
|
||||
|
||||
assert result == "hello world"
|
||||
mock_prompt_session.prompt_async.assert_called_once()
|
||||
@@ -38,23 +38,23 @@ async def test_read_interactive_input_async_handles_eof(mock_prompt_session):
|
||||
mock_prompt_session.prompt_async.side_effect = EOFError()
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
await commands._read_interactive_input_async()
|
||||
await terminal._read_interactive_input_async()
|
||||
|
||||
|
||||
def test_init_prompt_session_creates_session():
|
||||
"""Test that _init_prompt_session initializes the global session."""
|
||||
# Ensure global is None before test
|
||||
commands._PROMPT_SESSION = None
|
||||
terminal._prompt_session = None
|
||||
|
||||
with patch("nanobot.cli.commands.PromptSession") as mock_session_cls, \
|
||||
patch("nanobot.cli.commands.FileHistory"), \
|
||||
with patch("nanobot.cli.terminal.PromptSession") as mock_session_cls, \
|
||||
patch("nanobot.cli.terminal.FileHistory"), \
|
||||
patch("pathlib.Path.home") as mock_home:
|
||||
|
||||
mock_home.return_value = MagicMock()
|
||||
|
||||
commands._init_prompt_session()
|
||||
terminal._init_prompt_session()
|
||||
|
||||
assert commands._PROMPT_SESSION is not None
|
||||
assert terminal._prompt_session is not None
|
||||
mock_session_cls.assert_called_once()
|
||||
_, kwargs = mock_session_cls.call_args
|
||||
# Buffer is multiline-capable so Alt+Enter can insert newlines;
|
||||
@@ -68,7 +68,7 @@ def test_cli_key_bindings_enter_submits_and_alt_enter_newlines():
|
||||
"""Enter submits the buffer; Alt+Enter inserts a newline."""
|
||||
from prompt_toolkit.keys import Keys
|
||||
|
||||
kb = commands._build_cli_key_bindings()
|
||||
kb = terminal._build_cli_key_bindings()
|
||||
|
||||
def _keys(binding):
|
||||
return tuple(getattr(k, "value", k) for k in binding.keys)
|
||||
@@ -102,8 +102,8 @@ async def test_raw_lf_enter_still_submits_like_wsl_terminals():
|
||||
|
||||
with create_pipe_input() as pipe_input:
|
||||
with create_app_session(input=pipe_input, output=DummyOutput()):
|
||||
commands._init_prompt_session()
|
||||
session = commands._PROMPT_SESSION
|
||||
terminal._init_prompt_session()
|
||||
session = terminal._prompt_session
|
||||
pipe_input.send_text("hello\x0aworld\r")
|
||||
result = await session.prompt_async("> ")
|
||||
|
||||
@@ -119,8 +119,8 @@ async def test_alt_enter_inserts_newline_on_lf_terminals():
|
||||
|
||||
with create_pipe_input() as pipe_input:
|
||||
with create_app_session(input=pipe_input, output=DummyOutput()):
|
||||
commands._init_prompt_session()
|
||||
session = commands._PROMPT_SESSION
|
||||
terminal._init_prompt_session()
|
||||
session = terminal._prompt_session
|
||||
pipe_input.send_text("foo\x1b\x0abar\r")
|
||||
result = await session.prompt_async("> ")
|
||||
|
||||
@@ -136,8 +136,8 @@ async def test_csi_u_shift_enter_inserts_newline_not_raw_escape():
|
||||
|
||||
with create_pipe_input() as pipe_input:
|
||||
with create_app_session(input=pipe_input, output=DummyOutput()):
|
||||
commands._init_prompt_session()
|
||||
session = commands._PROMPT_SESSION
|
||||
terminal._init_prompt_session()
|
||||
session = terminal._prompt_session
|
||||
pipe_input.send_text("foo\x1b[13;2ubar\r")
|
||||
result = await session.prompt_async("> ")
|
||||
|
||||
@@ -173,10 +173,10 @@ def test_print_cli_progress_line_pauses_spinner_before_printing():
|
||||
mock_console = MagicMock()
|
||||
mock_console.status.return_value = spinner
|
||||
|
||||
with patch.object(commands.console, "print", side_effect=lambda *_args, **_kwargs: order.append("print")):
|
||||
with patch.object(terminal.console, "print", side_effect=lambda *_args, **_kwargs: order.append("print")):
|
||||
thinking = stream_mod.ThinkingSpinner(console=mock_console)
|
||||
with thinking:
|
||||
commands._print_cli_progress_line("tool running", thinking)
|
||||
terminal._print_cli_progress_line("tool running", thinking)
|
||||
|
||||
assert order == ["start", "stop", "print", "start", "stop"]
|
||||
|
||||
@@ -224,7 +224,7 @@ def test_print_cli_progress_line_opens_renderer_header_before_trace():
|
||||
renderer.ensure_header.side_effect = lambda: order.append("header")
|
||||
renderer.pause_spinner.return_value = nullcontext()
|
||||
|
||||
commands._print_cli_progress_line("tool running", None, renderer)
|
||||
terminal._print_cli_progress_line("tool running", None, renderer)
|
||||
|
||||
assert order == ["header", "print"]
|
||||
|
||||
@@ -235,7 +235,7 @@ def test_print_cli_progress_line_stops_live_before_trace():
|
||||
renderer = stream_mod.StreamRenderer(show_spinner=False)
|
||||
renderer._live = mock_live
|
||||
|
||||
commands._print_cli_progress_line("tool running", None, renderer)
|
||||
terminal._print_cli_progress_line("tool running", None, renderer)
|
||||
|
||||
mock_live.stop.assert_called_once()
|
||||
assert renderer._live is None
|
||||
@@ -254,10 +254,10 @@ async def test_print_interactive_progress_line_pauses_spinner_before_printing():
|
||||
async def fake_print(_text: str) -> None:
|
||||
order.append("print")
|
||||
|
||||
with patch("nanobot.cli.commands._print_interactive_line", side_effect=fake_print):
|
||||
with patch("nanobot.cli.terminal._print_interactive_line", side_effect=fake_print):
|
||||
thinking = stream_mod.ThinkingSpinner(console=mock_console)
|
||||
with thinking:
|
||||
await commands._print_interactive_progress_line("tool running", thinking)
|
||||
await terminal._print_interactive_progress_line("tool running", thinking)
|
||||
|
||||
assert order == ["start", "stop", "print", "start", "stop"]
|
||||
|
||||
@@ -269,7 +269,7 @@ def test_response_renderable_uses_text_for_explicit_plain_rendering():
|
||||
"📊 Tokens: 20639 in / 29 out"
|
||||
)
|
||||
|
||||
renderable = commands._response_renderable(
|
||||
renderable = terminal._response_renderable(
|
||||
status,
|
||||
render_markdown=True,
|
||||
metadata={"render_as": "text"},
|
||||
@@ -279,7 +279,7 @@ def test_response_renderable_uses_text_for_explicit_plain_rendering():
|
||||
|
||||
|
||||
def test_response_renderable_preserves_normal_markdown_rendering():
|
||||
renderable = commands._response_renderable("**bold**", render_markdown=True)
|
||||
renderable = terminal._response_renderable("**bold**", render_markdown=True)
|
||||
|
||||
assert renderable.__class__.__name__ == "Markdown"
|
||||
|
||||
@@ -287,7 +287,7 @@ def test_response_renderable_preserves_normal_markdown_rendering():
|
||||
def test_response_renderable_without_metadata_keeps_markdown_path():
|
||||
help_text = "🐈 nanobot commands:\n/status — Show bot status\n/help — Show available commands"
|
||||
|
||||
renderable = commands._response_renderable(help_text, render_markdown=True)
|
||||
renderable = terminal._response_renderable(help_text, render_markdown=True)
|
||||
|
||||
assert renderable.__class__.__name__ == "Markdown"
|
||||
|
||||
@@ -389,9 +389,9 @@ def test_render_interactive_ansi_force_terminal_follows_isatty():
|
||||
captured["console"] = c
|
||||
|
||||
with patch.object(sys.stdout, "isatty", return_value=True):
|
||||
commands._render_interactive_ansi(render_fn)
|
||||
terminal._render_interactive_ansi(render_fn)
|
||||
assert captured["console"]._force_terminal is True
|
||||
|
||||
with patch.object(sys.stdout, "isatty", return_value=False):
|
||||
commands._render_interactive_ansi(render_fn)
|
||||
terminal._render_interactive_ansi(render_fn)
|
||||
assert captured["console"]._force_terminal is False
|
||||
|
||||
Reference in New Issue
Block a user