refactor(cli): split commands into focused modules (#5175)

This commit is contained in:
chengyongru
2026-07-30 15:01:35 +08:00
committed by GitHub
parent ad6900e56c
commit e2563e2e74
24 changed files with 3190 additions and 2781 deletions
+28 -28
View File
@@ -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