mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 21:08:34 +03:00
refactor(cli): split commands into focused modules (#5175)
This commit is contained in:
@@ -356,7 +356,7 @@ class TestAutoCompact:
|
||||
loop.sessions.save(s2)
|
||||
|
||||
loop.consolidator.compact_idle_session = _make_fake_compact(loop)
|
||||
loop.auto_compact.check_expired(loop._schedule_background, loop.runtime_for_session)
|
||||
loop.auto_compact.check_expired(loop.schedule_background, loop.runtime_for_session)
|
||||
await _drain_background_tasks(loop)
|
||||
|
||||
active_after = loop.sessions.get_or_create("cli:active")
|
||||
@@ -836,7 +836,7 @@ class TestProactiveAutoCompact:
|
||||
async def _run_check_expired(loop, active_session_keys=()):
|
||||
"""Helper: run check_expired via callback and wait for background tasks."""
|
||||
loop.auto_compact.check_expired(
|
||||
loop._schedule_background,
|
||||
loop.schedule_background,
|
||||
loop.runtime_for_session,
|
||||
active_session_keys=active_session_keys,
|
||||
)
|
||||
@@ -976,12 +976,12 @@ class TestProactiveAutoCompact:
|
||||
loop.consolidator.compact_idle_session = _slow_compact
|
||||
|
||||
# First call starts archiving via callback
|
||||
loop.auto_compact.check_expired(loop._schedule_background, loop.runtime_for_session)
|
||||
loop.auto_compact.check_expired(loop.schedule_background, loop.runtime_for_session)
|
||||
await started.wait()
|
||||
assert archive_count == 1
|
||||
|
||||
# Second call should skip (key is in _archiving)
|
||||
loop.auto_compact.check_expired(loop._schedule_background, loop.runtime_for_session)
|
||||
loop.auto_compact.check_expired(loop.schedule_background, loop.runtime_for_session)
|
||||
assert archive_count == 1
|
||||
|
||||
# Clean up
|
||||
|
||||
@@ -215,7 +215,7 @@ async def test_preflight_consolidation_receives_pending_summary(tmp_path) -> Non
|
||||
return_value=(session, "Previous conversation summary: earlier context")
|
||||
) # type: ignore[method-assign]
|
||||
loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
|
||||
runtime = loop.llm_runtime()
|
||||
await loop.process_direct("hello", session_key="cli:test", runtime=runtime)
|
||||
@@ -252,7 +252,7 @@ async def test_preflight_consolidation_before_llm_call(tmp_path, monkeypatch) ->
|
||||
return LLMResponse(content="ok", tool_calls=[])
|
||||
loop.provider.chat_with_retry = track_llm
|
||||
loop.provider.chat_stream_with_retry = track_llm
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
|
||||
session = loop.sessions.get_or_create("cli:test")
|
||||
session.messages = [
|
||||
|
||||
@@ -33,7 +33,7 @@ def _make_loop(tmp_path):
|
||||
WebuiTurnCoordinator(
|
||||
bus=bus,
|
||||
sessions=loop.sessions,
|
||||
schedule_background=lambda coro: loop._schedule_background(coro),
|
||||
schedule_background=lambda coro: loop.schedule_background(coro),
|
||||
).subscribe(loop.runtime_events)
|
||||
loop.turn_delivery_factory.route_policy = WebuiTurnRoutePolicy(loop.sessions)
|
||||
loop.tools.get_definitions = MagicMock(return_value=[])
|
||||
|
||||
@@ -52,7 +52,7 @@ def _attach_webui_runtime_events(loop: AgentLoop, bus: MessageBus) -> None:
|
||||
coordinator = WebuiTurnCoordinator(
|
||||
bus=bus,
|
||||
sessions=loop.sessions,
|
||||
schedule_background=lambda coro: loop._schedule_background(coro),
|
||||
schedule_background=lambda coro: loop.schedule_background(coro),
|
||||
)
|
||||
coordinator.subscribe(loop.runtime_events)
|
||||
|
||||
@@ -1203,7 +1203,7 @@ class TestToolEventProgress:
|
||||
elif hasattr(coro, "close"):
|
||||
coro.close()
|
||||
|
||||
loop._schedule_background = schedule_background # type: ignore[method-assign]
|
||||
loop.schedule_background = schedule_background # type: ignore[method-assign]
|
||||
|
||||
await loop._dispatch(InboundMessage(
|
||||
channel="websocket",
|
||||
@@ -1249,7 +1249,7 @@ class TestToolEventProgress:
|
||||
fake_title_after_turn,
|
||||
)
|
||||
scheduled: list[object] = []
|
||||
loop._schedule_background = scheduled.append # type: ignore[method-assign]
|
||||
loop.schedule_background = scheduled.append # type: ignore[method-assign]
|
||||
|
||||
await loop._dispatch(InboundMessage(
|
||||
channel="websocket",
|
||||
|
||||
@@ -78,7 +78,7 @@ def _make_full_loop(tmp_path: Path) -> AgentLoop:
|
||||
WebuiTurnCoordinator(
|
||||
bus=loop.bus,
|
||||
sessions=loop.sessions,
|
||||
schedule_background=lambda coro: loop._schedule_background(coro),
|
||||
schedule_background=lambda coro: loop.schedule_background(coro),
|
||||
).subscribe(loop.runtime_events)
|
||||
return loop
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_sessions_run_concurrently_with_isolated_model_presets(tmp_path) -
|
||||
model_presets=presets,
|
||||
preset_snapshot_loader=load_preset,
|
||||
)
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.set_session_model_preset("sdk:fast", "fast")
|
||||
loop.set_session_model_preset("sdk:deep", "deep")
|
||||
|
||||
@@ -116,7 +116,7 @@ async def test_removed_session_model_preset_falls_back_and_clears_metadata(tmp_p
|
||||
model="base-model",
|
||||
context_window_tokens=8_000,
|
||||
)
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
session_key = "sdk:removed-preset"
|
||||
session = loop.sessions.get_or_create(session_key)
|
||||
session.metadata[SESSION_MODEL_PRESET_METADATA_KEY] = "removed"
|
||||
@@ -161,7 +161,7 @@ async def test_streamed_sdk_resolves_session_runtime_after_lock_admission(tmp_pa
|
||||
model_presets=presets,
|
||||
preset_snapshot_loader=load_preset,
|
||||
)
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
session_key = "sdk:queued"
|
||||
loop.set_session_model_preset(session_key, "fast")
|
||||
|
||||
@@ -198,7 +198,7 @@ async def test_sdk_custom_model_preset_metadata_does_not_select_runtime(
|
||||
model="base-model",
|
||||
context_window_tokens=8_000,
|
||||
)
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
bot = Nanobot(loop)
|
||||
|
||||
await bot.sessions.ingest(
|
||||
@@ -239,7 +239,7 @@ async def test_sdk_invalid_internal_model_preset_metadata_fails_explicitly(
|
||||
model="base-model",
|
||||
context_window_tokens=8_000,
|
||||
)
|
||||
loop._schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
loop.schedule_background = lambda coro: coro.close() # type: ignore[method-assign]
|
||||
bot = Nanobot(loop)
|
||||
|
||||
await bot.sessions.ingest(
|
||||
|
||||
@@ -246,7 +246,7 @@ class TestCmdNewUnifiedSession:
|
||||
assert len(sessions.get_or_create("unified:default").messages) == 2
|
||||
expected_snapshot = list(shared.messages)
|
||||
|
||||
# _schedule_background is a *sync* method that schedules a coroutine via
|
||||
# schedule_background is a *sync* method that schedules a coroutine via
|
||||
# asyncio.create_task(). Mirror that exactly so the coroutine is consumed
|
||||
# and no RuntimeWarning is emitted.
|
||||
admitted_runtime = MagicMock(name="admitted_runtime")
|
||||
@@ -255,8 +255,8 @@ class TestCmdNewUnifiedSession:
|
||||
consolidator=SimpleNamespace(archive=AsyncMock(return_value=True)),
|
||||
_cancel_active_tasks=AsyncMock(return_value=0),
|
||||
llm_runtime=MagicMock(return_value=MagicMock()),
|
||||
schedule_background=lambda coro: asyncio.ensure_future(coro),
|
||||
)
|
||||
loop._schedule_background = lambda coro: asyncio.ensure_future(coro)
|
||||
|
||||
msg = InboundMessage(
|
||||
channel="telegram", sender_id="user1", chat_id="111", content="/new",
|
||||
@@ -303,8 +303,8 @@ class TestCmdNewUnifiedSession:
|
||||
consolidator=SimpleNamespace(archive=AsyncMock(return_value=True)),
|
||||
_cancel_active_tasks=AsyncMock(return_value=0),
|
||||
runtime_for_session=MagicMock(return_value=MagicMock()),
|
||||
schedule_background=lambda coro: asyncio.ensure_future(coro),
|
||||
)
|
||||
loop._schedule_background = lambda coro: asyncio.ensure_future(coro)
|
||||
|
||||
msg = InboundMessage(
|
||||
channel="telegram", sender_id="user1", chat_id="111", content="/new",
|
||||
|
||||
+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
|
||||
|
||||
+147
-105
@@ -17,6 +17,11 @@ from nanobot.agent.tools.registry import ToolRegistry
|
||||
from nanobot.agent.turn_delivery import TurnDeliveryFactory
|
||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||
from nanobot.cli import commands as cli_commands
|
||||
from nanobot.cli import gateway_runtime as cli_gateway_runtime
|
||||
from nanobot.cli import provider as provider_commands
|
||||
from nanobot.cli import terminal as cli_terminal
|
||||
from nanobot.cli import webui as cli_webui
|
||||
from nanobot.cli import webui_support as cli_webui_support
|
||||
from nanobot.cli.commands import app
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.cron.service import CronJobSkippedError
|
||||
@@ -113,7 +118,7 @@ def test_gateway_signal_handler_first_signal_stops_and_second_forces() -> None:
|
||||
task = asyncio.create_task(never.wait())
|
||||
output: list[str] = []
|
||||
|
||||
restore = cli_commands._install_gateway_shutdown_handlers(
|
||||
restore = cli_gateway_runtime._install_gateway_shutdown_handlers(
|
||||
loop, shutdown_event, [task], output.append,
|
||||
)
|
||||
try:
|
||||
@@ -161,8 +166,8 @@ def test_interactive_tty_mode_restores_line_input(monkeypatch) -> None:
|
||||
attrs[3] &= ~(termios.ISIG | termios.ICANON | termios.ECHO)
|
||||
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
||||
|
||||
monkeypatch.setattr(cli_commands.sys, "stdin", _Stdin())
|
||||
cli_commands._ensure_interactive_tty_mode()
|
||||
monkeypatch.setattr(cli_terminal.sys, "stdin", _Stdin())
|
||||
cli_terminal._ensure_interactive_tty_mode()
|
||||
|
||||
restored = termios.tcgetattr(slave_fd)
|
||||
assert restored[0] & termios.ICRNL
|
||||
@@ -179,24 +184,24 @@ def test_webui_restores_tty_before_loading_config(monkeypatch, tmp_path: Path) -
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("{}", encoding="utf-8")
|
||||
calls: list[str] = []
|
||||
original_resolve = cli_commands._resolve_webui_config_path
|
||||
original_resolve = cli_webui._resolve_webui_config_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
cli_commands,
|
||||
cli_terminal,
|
||||
"_ensure_interactive_tty_mode",
|
||||
lambda: calls.append("tty"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli_commands,
|
||||
cli_webui,
|
||||
"_resolve_webui_config_path",
|
||||
lambda path: calls.append("config") or original_resolve(path),
|
||||
)
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr(cli_commands, "sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(cli_commands, "_gateway_health_ready", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_commands, "_webui_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_commands, "_tcp_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_commands, "_run_gateway", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(cli_webui, "sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(cli_webui, "_gateway_health_ready", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_webui, "_webui_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_webui, "_tcp_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(cli_webui, "_run_gateway", lambda *_args, **_kwargs: None)
|
||||
|
||||
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes", "--no-open"])
|
||||
|
||||
@@ -209,11 +214,11 @@ def test_disabled_dream_cursor_only_advances_when_behind(tmp_path) -> None:
|
||||
store.append_history("first")
|
||||
store.append_history("second")
|
||||
|
||||
cli_commands._advance_dream_cursor_if_behind(store)
|
||||
cli_gateway_runtime._advance_dream_cursor_if_behind(store)
|
||||
assert store.get_last_dream_cursor() == 2
|
||||
|
||||
store.set_last_dream_cursor(10)
|
||||
cli_commands._advance_dream_cursor_if_behind(store)
|
||||
cli_gateway_runtime._advance_dream_cursor_if_behind(store)
|
||||
assert store.get_last_dream_cursor() == 10
|
||||
|
||||
|
||||
@@ -225,7 +230,7 @@ def test_commit_dream_changes_skips_noop_run(tmp_path) -> None:
|
||||
store.git.auto_commit("initial")
|
||||
store.git.auto_commit = MagicMock(wraps=store.git.auto_commit)
|
||||
|
||||
assert cli_commands._commit_dream_changes(store) is None
|
||||
assert cli_gateway_runtime._commit_dream_changes(store) is None
|
||||
store.git.auto_commit.assert_not_called()
|
||||
|
||||
|
||||
@@ -238,7 +243,7 @@ def test_commit_dream_changes_commits_real_edits(tmp_path) -> None:
|
||||
store.write_memory("# Memory\n- Research notes")
|
||||
store.git.auto_commit = MagicMock(wraps=store.git.auto_commit)
|
||||
|
||||
sha = cli_commands._commit_dream_changes(store)
|
||||
sha = cli_gateway_runtime._commit_dream_changes(store)
|
||||
|
||||
assert sha is not None
|
||||
store.git.auto_commit.assert_called_once()
|
||||
@@ -493,7 +498,7 @@ def test_openai_codex_oauth_default_matches_curated_flagship():
|
||||
|
||||
assert spec is not None
|
||||
assert spec.builtin_models
|
||||
assert cli_commands._OAUTH_PROVIDER_DEFAULT_MODELS["openai_codex"] == (
|
||||
assert provider_commands._OAUTH_PROVIDER_DEFAULT_MODELS["openai_codex"] == (
|
||||
spec.builtin_models[0].id
|
||||
)
|
||||
|
||||
@@ -671,16 +676,28 @@ def test_provider_login_rejects_unknown_provider():
|
||||
assert "Unknown OAuth provider" in result.stdout
|
||||
|
||||
|
||||
def test_provider_login_openai_codex_handles_missing_oauth_symbol(monkeypatch):
|
||||
import oauth_cli_kit
|
||||
|
||||
monkeypatch.delattr(oauth_cli_kit, "get_token")
|
||||
|
||||
result = runner.invoke(app, ["provider", "login", "openai-codex"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "oauth_cli_kit not installed" in result.stdout
|
||||
assert result.exception is not None
|
||||
|
||||
|
||||
def test_provider_login_can_set_openai_codex_as_main_provider(tmp_path):
|
||||
config_path = tmp_path / "config.json"
|
||||
called = False
|
||||
original = cli_commands._LOGIN_HANDLERS["openai_codex"]
|
||||
original = provider_commands._LOGIN_HANDLERS["openai_codex"]
|
||||
|
||||
def fake_login() -> None:
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
cli_commands._LOGIN_HANDLERS["openai_codex"] = fake_login
|
||||
provider_commands._LOGIN_HANDLERS["openai_codex"] = fake_login
|
||||
try:
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -694,7 +711,7 @@ def test_provider_login_can_set_openai_codex_as_main_provider(tmp_path):
|
||||
],
|
||||
)
|
||||
finally:
|
||||
cli_commands._LOGIN_HANDLERS["openai_codex"] = original
|
||||
provider_commands._LOGIN_HANDLERS["openai_codex"] = original
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert called is True
|
||||
@@ -709,8 +726,8 @@ def test_provider_login_can_set_openai_codex_as_main_provider(tmp_path):
|
||||
|
||||
def test_provider_login_can_set_github_copilot_as_main_provider(tmp_path):
|
||||
config_path = tmp_path / "config.json"
|
||||
original = cli_commands._LOGIN_HANDLERS["github_copilot"]
|
||||
cli_commands._LOGIN_HANDLERS["github_copilot"] = lambda: None
|
||||
original = provider_commands._LOGIN_HANDLERS["github_copilot"]
|
||||
provider_commands._LOGIN_HANDLERS["github_copilot"] = lambda: None
|
||||
try:
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -724,7 +741,7 @@ def test_provider_login_can_set_github_copilot_as_main_provider(tmp_path):
|
||||
],
|
||||
)
|
||||
finally:
|
||||
cli_commands._LOGIN_HANDLERS["github_copilot"] = original
|
||||
provider_commands._LOGIN_HANDLERS["github_copilot"] = original
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Set github-copilot as the main provider" in result.stdout
|
||||
@@ -738,8 +755,8 @@ def test_provider_login_can_set_github_copilot_as_main_provider(tmp_path):
|
||||
|
||||
def test_provider_login_can_set_xai_grok_as_main_provider(tmp_path):
|
||||
config_path = tmp_path / "config.json"
|
||||
original = cli_commands._LOGIN_HANDLERS["xai_grok"]
|
||||
cli_commands._LOGIN_HANDLERS["xai_grok"] = lambda: None
|
||||
original = provider_commands._LOGIN_HANDLERS["xai_grok"]
|
||||
provider_commands._LOGIN_HANDLERS["xai_grok"] = lambda: None
|
||||
try:
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -753,7 +770,7 @@ def test_provider_login_can_set_xai_grok_as_main_provider(tmp_path):
|
||||
],
|
||||
)
|
||||
finally:
|
||||
cli_commands._LOGIN_HANDLERS["xai_grok"] = original
|
||||
provider_commands._LOGIN_HANDLERS["xai_grok"] = original
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Set xai-grok as the main provider" in result.stdout
|
||||
@@ -768,8 +785,8 @@ def test_provider_login_can_set_xai_grok_as_main_provider(tmp_path):
|
||||
|
||||
def test_provider_login_model_implies_set_main_provider(tmp_path):
|
||||
config_path = tmp_path / "config.json"
|
||||
original = cli_commands._LOGIN_HANDLERS["github_copilot"]
|
||||
cli_commands._LOGIN_HANDLERS["github_copilot"] = lambda: None
|
||||
original = provider_commands._LOGIN_HANDLERS["github_copilot"]
|
||||
provider_commands._LOGIN_HANDLERS["github_copilot"] = lambda: None
|
||||
try:
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -784,7 +801,7 @@ def test_provider_login_model_implies_set_main_provider(tmp_path):
|
||||
],
|
||||
)
|
||||
finally:
|
||||
cli_commands._LOGIN_HANDLERS["github_copilot"] = original
|
||||
provider_commands._LOGIN_HANDLERS["github_copilot"] = original
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Set github-copilot as the main provider" in result.stdout
|
||||
@@ -1470,12 +1487,12 @@ def mock_agent_runtime(tmp_path):
|
||||
|
||||
with patch("nanobot.config.loader.load_config", return_value=config) as mock_load_config, \
|
||||
patch("nanobot.config.loader.resolve_config_env_vars", side_effect=lambda c: c), \
|
||||
patch("nanobot.cli.commands.sync_workspace_templates") as mock_sync_templates, \
|
||||
patch("nanobot.cli.agent.sync_workspace_templates") as mock_sync_templates, \
|
||||
patch("nanobot.providers.factory.make_provider", return_value=_fake_provider()), \
|
||||
patch("nanobot.cli.commands._print_agent_response") as mock_print_response, \
|
||||
patch("nanobot.cli.terminal._print_agent_response") as mock_print_response, \
|
||||
patch("nanobot.bus.queue.MessageBus"), \
|
||||
patch("nanobot.cron.service.CronService"), \
|
||||
patch("nanobot.cli.commands.AgentLoop.from_config") as mock_from_config:
|
||||
patch("nanobot.cli.agent.AgentLoop.from_config") as mock_from_config:
|
||||
agent_loop = MagicMock()
|
||||
agent_loop.channels_config = None
|
||||
agent_loop.process_direct = AsyncMock(
|
||||
@@ -1544,7 +1561,7 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None:
|
||||
lambda path: seen.__setitem__("config_path", path),
|
||||
)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", lambda _store: object())
|
||||
@@ -1562,8 +1579,8 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None:
|
||||
async def close_mcp(self) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.terminal._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
|
||||
result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
|
||||
|
||||
@@ -1582,7 +1599,7 @@ def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Pa
|
||||
|
||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
||||
|
||||
@@ -1604,8 +1621,8 @@ def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Pa
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.terminal._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
|
||||
result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
|
||||
|
||||
@@ -1631,7 +1648,7 @@ def test_agent_workspace_override_does_not_migrate_legacy_cron(
|
||||
|
||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
||||
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
||||
@@ -1654,8 +1671,8 @@ def test_agent_workspace_override_does_not_migrate_legacy_cron(
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.terminal._print_agent_response", lambda *_args, **_kwargs: None)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -1687,7 +1704,7 @@ def test_agent_custom_config_workspace_does_not_migrate_legacy_cron(
|
||||
|
||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.agent.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider())
|
||||
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
||||
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
||||
@@ -1710,9 +1727,9 @@ def test_agent_custom_config_workspace_does_not_migrate_legacy_cron(
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.agent.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None
|
||||
"nanobot.cli.terminal._print_agent_response", lambda *_args, **_kwargs: None
|
||||
)
|
||||
|
||||
result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
|
||||
@@ -1774,20 +1791,20 @@ def test_heartbeat_retains_recent_messages_by_default():
|
||||
],
|
||||
)
|
||||
def test_heartbeat_has_active_tasks(content, expected):
|
||||
from nanobot.cli.commands import _heartbeat_has_active_tasks
|
||||
from nanobot.cli.gateway_runtime import _heartbeat_has_active_tasks
|
||||
|
||||
assert _heartbeat_has_active_tasks(content) is expected
|
||||
|
||||
|
||||
def test_heartbeat_skips_bundled_template():
|
||||
from nanobot.cli.commands import _heartbeat_has_active_tasks
|
||||
from nanobot.cli.gateway_runtime import _heartbeat_has_active_tasks
|
||||
from nanobot.utils.helpers import load_bundled_template
|
||||
|
||||
assert _heartbeat_has_active_tasks(load_bundled_template("HEARTBEAT.md")) is False
|
||||
|
||||
|
||||
def test_heartbeat_target_skips_archived_webui_sessions():
|
||||
from nanobot.cli.commands import _pick_heartbeat_target_from_sessions
|
||||
from nanobot.cli.gateway_runtime import _pick_heartbeat_target_from_sessions
|
||||
|
||||
target = _pick_heartbeat_target_from_sessions(
|
||||
enabled_channels=["websocket"],
|
||||
@@ -1802,7 +1819,7 @@ def test_heartbeat_target_skips_archived_webui_sessions():
|
||||
|
||||
|
||||
def test_heartbeat_target_uses_last_channel_for_unified_session():
|
||||
from nanobot.cli.commands import _pick_heartbeat_target_from_sessions
|
||||
from nanobot.cli.gateway_runtime import _pick_heartbeat_target_from_sessions
|
||||
from nanobot.session.keys import LAST_CHANNEL_METADATA_KEY, UNIFIED_SESSION_KEY
|
||||
|
||||
target = _pick_heartbeat_target_from_sessions(
|
||||
@@ -1824,7 +1841,7 @@ def test_heartbeat_target_uses_last_channel_for_unified_session():
|
||||
],
|
||||
)
|
||||
def test_heartbeat_target_rejects_unroutable_unified_metadata(metadata):
|
||||
from nanobot.cli.commands import _pick_heartbeat_target_from_sessions
|
||||
from nanobot.cli.gateway_runtime import _pick_heartbeat_target_from_sessions
|
||||
from nanobot.session.keys import UNIFIED_SESSION_KEY
|
||||
|
||||
target = _pick_heartbeat_target_from_sessions(
|
||||
@@ -1865,9 +1882,17 @@ def _patch_webui_provider_ready(monkeypatch) -> None:
|
||||
|
||||
|
||||
def _patch_gateway_ports_free(monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.cli.commands._gateway_health_ready", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr("nanobot.cli.commands._tcp_endpoint_reachable", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr("nanobot.cli.commands._webui_endpoint_reachable", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr("nanobot.cli.webui._tcp_endpoint_reachable", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_a, **_kw: False)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.gateway_runtime._tcp_endpoint_reachable",
|
||||
lambda *_a, **_kw: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.gateway_runtime._webui_endpoint_reachable",
|
||||
lambda *_a, **_kw: False,
|
||||
)
|
||||
|
||||
|
||||
def _patch_cli_command_runtime(
|
||||
@@ -1894,6 +1919,14 @@ def _patch_cli_command_runtime(
|
||||
"nanobot.cli.commands.sync_workspace_templates",
|
||||
sync_templates or (lambda _path: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui.sync_workspace_templates",
|
||||
sync_templates or (lambda _path: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.gateway_runtime.sync_workspace_templates",
|
||||
sync_templates or (lambda _path: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.providers.factory.make_provider",
|
||||
provider_factory,
|
||||
@@ -1907,7 +1940,7 @@ def _patch_cli_command_runtime(
|
||||
lambda _config_path=None: _test_provider_snapshot(provider_factory(config), config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._provider_setup_error",
|
||||
"nanobot.cli.webui_support._provider_setup_error",
|
||||
lambda _config: None,
|
||||
)
|
||||
_patch_gateway_ports_free(monkeypatch)
|
||||
@@ -2008,10 +2041,10 @@ def test_heartbeat_empty_response_still_retains_recent_messages(
|
||||
session_manager=_FakeSessionManager,
|
||||
cron_service=_FakeCron,
|
||||
)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||
monkeypatch.setattr("nanobot.cli.commands.read_webui_sidebar_state", lambda: {})
|
||||
monkeypatch.setattr("nanobot.cli.commands.evaluate_response", _unexpected_evaluator)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.read_webui_sidebar_state", lambda: {})
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.evaluate_response", _unexpected_evaluator)
|
||||
|
||||
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||
|
||||
@@ -2034,7 +2067,7 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands.sync_workspace_templates",
|
||||
"nanobot.cli.webui.sync_workspace_templates",
|
||||
lambda path: seen.__setitem__("templates", path),
|
||||
)
|
||||
|
||||
@@ -2042,7 +2075,7 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
|
||||
seen["gateway_config"] = config
|
||||
seen["gateway_kwargs"] = kwargs
|
||||
|
||||
monkeypatch.setattr("nanobot.cli.commands._run_gateway", _fake_run_gateway)
|
||||
monkeypatch.setattr("nanobot.cli.webui._run_gateway", _fake_run_gateway)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
@@ -2091,13 +2124,17 @@ def test_webui_yes_starts_first_run_without_provider_setup(monkeypatch, tmp_path
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._provider_setup_error",
|
||||
"nanobot.cli.webui_support._provider_setup_error",
|
||||
lambda _config: "No API key configured for provider 'custom'.",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui._provider_setup_error",
|
||||
lambda _config: "No API key configured for provider 'custom'.",
|
||||
)
|
||||
_patch_gateway_ports_free(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._run_gateway",
|
||||
"nanobot.cli.webui._run_gateway",
|
||||
lambda config, **kwargs: seen.update(config=config, **kwargs),
|
||||
)
|
||||
|
||||
@@ -2135,7 +2172,7 @@ def test_webui_missing_runtime_env_fails_before_starting_gateway(
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._run_gateway",
|
||||
"nanobot.cli.webui._run_gateway",
|
||||
lambda *_args, **_kwargs: pytest.fail("gateway must not start with unresolved config"),
|
||||
)
|
||||
|
||||
@@ -2189,9 +2226,9 @@ def test_webui_background_starts_runtime_and_opens_browser(monkeypatch, tmp_path
|
||||
config_file.write_text("{}")
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._prepare_webui_bundle_for_gateway",
|
||||
"nanobot.cli.webui._prepare_webui_bundle_for_gateway",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
@@ -2213,7 +2250,7 @@ def test_webui_background_starts_runtime_and_opens_browser(monkeypatch, tmp_path
|
||||
|
||||
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._open_webui_browser",
|
||||
"nanobot.cli.webui._open_webui_browser",
|
||||
lambda url: seen.__setitem__("opened_url", url),
|
||||
)
|
||||
|
||||
@@ -2256,7 +2293,7 @@ def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> Non
|
||||
url = "http://127.0.0.1:8765/#/?bootstrapSecret=super-secret"
|
||||
monkeypatch.setattr("webbrowser.open", lambda value: opened.append(value))
|
||||
|
||||
cli_commands._open_webui_browser(url, wait=False)
|
||||
cli_webui_support._open_webui_browser(url, wait=False)
|
||||
|
||||
assert opened == [url]
|
||||
output = _strip_ansi(capsys.readouterr().out)
|
||||
@@ -2275,9 +2312,9 @@ def test_webui_background_restarts_when_config_changes_and_gateway_is_running(
|
||||
config_file.write_text("{}")
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._prepare_webui_bundle_for_gateway",
|
||||
"nanobot.cli.webui._prepare_webui_bundle_for_gateway",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
@@ -2306,7 +2343,7 @@ def test_webui_background_restarts_when_config_changes_and_gateway_is_running(
|
||||
|
||||
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._open_webui_browser",
|
||||
"nanobot.cli.webui._open_webui_browser",
|
||||
lambda url: seen.__setitem__("opened_url", url),
|
||||
)
|
||||
|
||||
@@ -2347,15 +2384,15 @@ def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_
|
||||
config_file.write_text("{}")
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.commands._gateway_health_ready", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr("nanobot.cli.commands._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._open_webui_browser",
|
||||
"nanobot.cli.webui._open_webui_browser",
|
||||
lambda url, **kwargs: seen.update({"opened_url": url, "open_kwargs": kwargs}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._run_gateway",
|
||||
"nanobot.cli.webui._run_gateway",
|
||||
lambda *_args, **_kwargs: pytest.fail("existing gateway should be reused"),
|
||||
)
|
||||
|
||||
@@ -2368,7 +2405,7 @@ def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_
|
||||
|
||||
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._attach_to_background_gateway",
|
||||
"nanobot.cli.webui._attach_to_background_gateway",
|
||||
lambda runtime: seen.__setitem__("attached_runtime", runtime),
|
||||
)
|
||||
|
||||
@@ -2401,9 +2438,9 @@ def test_attach_to_background_gateway_stops_on_ctrl_c(monkeypatch, capsys) -> No
|
||||
def _interrupt(_seconds: float) -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
monkeypatch.setattr("nanobot.cli.commands.time.sleep", _interrupt)
|
||||
monkeypatch.setattr("nanobot.cli.webui_support.time.sleep", _interrupt)
|
||||
|
||||
cli_commands._attach_to_background_gateway(_FakeRuntime())
|
||||
cli_webui_support._attach_to_background_gateway(_FakeRuntime())
|
||||
|
||||
assert stopped is True
|
||||
output = capsys.readouterr().out
|
||||
@@ -2416,12 +2453,12 @@ def test_webui_foreground_does_not_claim_unmanaged_gateway(monkeypatch, tmp_path
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("{}")
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.commands._gateway_health_ready", lambda *_args: True)
|
||||
monkeypatch.setattr("nanobot.cli.commands._webui_endpoint_reachable", lambda *_args: True)
|
||||
monkeypatch.setattr("nanobot.cli.commands._open_webui_browser", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args: True)
|
||||
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args: True)
|
||||
monkeypatch.setattr("nanobot.cli.webui._open_webui_browser", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._attach_to_background_gateway",
|
||||
"nanobot.cli.webui._attach_to_background_gateway",
|
||||
lambda _runtime: pytest.fail("unmanaged gateway must not be attached"),
|
||||
)
|
||||
|
||||
@@ -2444,12 +2481,12 @@ def test_webui_foreground_refuses_occupied_webui_port(monkeypatch, tmp_path: Pat
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("{}")
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.commands._gateway_health_ready", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr("nanobot.cli.commands._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr("nanobot.cli.commands._tcp_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
|
||||
monkeypatch.setattr("nanobot.cli.webui._tcp_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._run_gateway",
|
||||
"nanobot.cli.webui._run_gateway",
|
||||
lambda *_args, **_kwargs: pytest.fail("gateway should not start on occupied ports"),
|
||||
)
|
||||
|
||||
@@ -2596,9 +2633,9 @@ def test_gateway_unbound_agent_cron_is_skipped(
|
||||
|
||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: provider)
|
||||
monkeypatch.setattr("nanobot.cli.commands._provider_setup_error", lambda _config: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui_support._provider_setup_error", lambda _config: None)
|
||||
_patch_gateway_ports_free(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.providers.factory.build_provider_snapshot",
|
||||
@@ -2672,10 +2709,10 @@ def test_gateway_unbound_agent_cron_is_skipped(
|
||||
raise AssertionError("unbound cron job must not be evaluated for delivery")
|
||||
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _StopAfterCronSetup)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands.evaluate_response",
|
||||
"nanobot.cli.gateway_runtime.evaluate_response",
|
||||
_capture_evaluate_response,
|
||||
)
|
||||
|
||||
@@ -2724,9 +2761,9 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
||||
|
||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: provider)
|
||||
monkeypatch.setattr("nanobot.cli.commands._provider_setup_error", lambda _config: None)
|
||||
monkeypatch.setattr("nanobot.cli.webui_support._provider_setup_error", lambda _config: None)
|
||||
_patch_gateway_ports_free(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.providers.factory.build_provider_snapshot",
|
||||
@@ -2788,9 +2825,9 @@ def test_gateway_bound_cron_runs_as_session_turn(
|
||||
raise AssertionError("bound cron must not use legacy response evaluator")
|
||||
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _StopAfterCronSetup)
|
||||
monkeypatch.setattr("nanobot.cli.commands.evaluate_response", _unexpected_evaluator)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.evaluate_response", _unexpected_evaluator)
|
||||
|
||||
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||
assert isinstance(result.exception, _StopGatewayError)
|
||||
@@ -2984,7 +3021,7 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
|
||||
self.runtime_resolver = MagicMock()
|
||||
seen["agent"] = self
|
||||
|
||||
def _schedule_background(self, _coro) -> None:
|
||||
def schedule_background(self, _coro) -> None:
|
||||
return None
|
||||
|
||||
async def run(self) -> None:
|
||||
@@ -3016,7 +3053,7 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
|
||||
seen["local_trigger_queue_kwargs"] = kwargs
|
||||
raise _StopGatewayError("stop")
|
||||
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.triggers.local_runner.run_local_trigger_queue",
|
||||
@@ -3124,7 +3161,7 @@ def test_gateway_custom_config_workspace_does_not_migrate_legacy_cron(
|
||||
|
||||
def test_migrate_cron_store_moves_legacy_file(tmp_path: Path) -> None:
|
||||
"""Legacy global jobs.json is moved into the workspace on first run."""
|
||||
from nanobot.cli.commands import _migrate_cron_store
|
||||
from nanobot.cli.runtime_config import _migrate_cron_store
|
||||
|
||||
legacy_dir = tmp_path / "global" / "cron"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
@@ -3145,7 +3182,7 @@ def test_migrate_cron_store_moves_legacy_file(tmp_path: Path) -> None:
|
||||
|
||||
def test_migrate_cron_store_skips_when_workspace_file_exists(tmp_path: Path) -> None:
|
||||
"""Migration does not overwrite an existing workspace cron store."""
|
||||
from nanobot.cli.commands import _migrate_cron_store
|
||||
from nanobot.cli.runtime_config import _migrate_cron_store
|
||||
|
||||
legacy_dir = tmp_path / "global" / "cron"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
@@ -3312,7 +3349,7 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses(
|
||||
message_bus=lambda: object(),
|
||||
session_manager=lambda _workspace: object(),
|
||||
)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCronService)
|
||||
monkeypatch.setattr("asyncio.start_server", _fake_start_server)
|
||||
@@ -3363,13 +3400,14 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses(
|
||||
async def read(self, _size: int) -> bytes:
|
||||
nonlocal started
|
||||
started += 1
|
||||
if started == cli_commands._GATEWAY_HEALTH_MAX_CONNECTIONS:
|
||||
if started == cli_gateway_runtime._GATEWAY_HEALTH_MAX_CONNECTIONS:
|
||||
all_started.set()
|
||||
await release.wait()
|
||||
return b"GET /health HTTP/1.1\r\n\r\n"
|
||||
|
||||
active_writers = [
|
||||
_FakeWriter() for _ in range(cli_commands._GATEWAY_HEALTH_MAX_CONNECTIONS)
|
||||
_FakeWriter()
|
||||
for _ in range(cli_gateway_runtime._GATEWAY_HEALTH_MAX_CONNECTIONS)
|
||||
]
|
||||
active_tasks = [
|
||||
asyncio.create_task(health_handler(_BlockingReader(), writer))
|
||||
@@ -3394,7 +3432,11 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses(
|
||||
async def read(self, _size: int) -> bytes:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
monkeypatch.setattr(cli_commands, "_GATEWAY_HEALTH_READ_TIMEOUT_SECONDS", 0.01)
|
||||
monkeypatch.setattr(
|
||||
cli_gateway_runtime,
|
||||
"_GATEWAY_HEALTH_READ_TIMEOUT_SECONDS",
|
||||
0.01,
|
||||
)
|
||||
timed_out_writer = _FakeWriter()
|
||||
asyncio.run(health_handler(_NeverRespondingReader(), timed_out_writer))
|
||||
assert timed_out_writer.closed is True
|
||||
@@ -3485,7 +3527,7 @@ def test_gateway_shutdown_lets_agent_task_own_mcp_cleanup(
|
||||
message_bus=lambda: object(),
|
||||
session_manager=lambda _workspace: object(),
|
||||
)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCronService)
|
||||
monkeypatch.setattr("asyncio.start_server", _fake_start_server)
|
||||
@@ -3601,12 +3643,12 @@ def test_gateway_shutdown_event_exits_forever_runtime_tasks(
|
||||
message_bus=lambda: object(),
|
||||
session_manager=lambda _workspace: object(),
|
||||
)
|
||||
monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.cli.gateway_runtime.AgentLoop", _FakeAgentLoop)
|
||||
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager)
|
||||
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCronService)
|
||||
monkeypatch.setattr("asyncio.start_server", _fake_start_server)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands._install_gateway_shutdown_handlers",
|
||||
"nanobot.cli.gateway_runtime._install_gateway_shutdown_handlers",
|
||||
_fake_install_shutdown_handlers,
|
||||
)
|
||||
|
||||
|
||||
@@ -413,7 +413,7 @@ def test_gateway_missing_provider_managed_start_for_webui_setup(
|
||||
monkeypatch.setattr(GatewayRuntime, "start_background", fake_start_background)
|
||||
monkeypatch.setattr(GatewayRuntime, "restart", fake_restart)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.commands.ensure_webui_bundle",
|
||||
"nanobot.cli.webui_support.ensure_webui_bundle",
|
||||
lambda **_kwargs: None,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from nanobot.bus.outbound_events import ProgressEvent, RetryWaitEvent
|
||||
from nanobot.cli import commands
|
||||
from nanobot.cli import terminal
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -22,8 +22,8 @@ async def test_interactive_retry_wait_is_rendered_as_progress_even_when_progress
|
||||
async def fake_print(text: str, active_thinking: object | None, renderer=None) -> None:
|
||||
calls.append((text, active_thinking))
|
||||
|
||||
with patch("nanobot.cli.commands._print_interactive_progress_line", side_effect=fake_print):
|
||||
handled = await commands._maybe_print_interactive_progress(
|
||||
with patch("nanobot.cli.terminal._print_interactive_progress_line", side_effect=fake_print):
|
||||
handled = await terminal._maybe_print_interactive_progress(
|
||||
msg,
|
||||
thinking,
|
||||
channels_config,
|
||||
@@ -46,8 +46,8 @@ async def test_reasoning_displayed_when_show_reasoning_enabled():
|
||||
metadata={},
|
||||
)
|
||||
|
||||
with patch("nanobot.cli.commands._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
handled = await commands._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
with patch("nanobot.cli.terminal._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
handled = await terminal._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
|
||||
assert handled is True
|
||||
assert calls == ["Let me think about this..."]
|
||||
@@ -66,8 +66,8 @@ async def test_reasoning_delta_displayed_when_show_reasoning_enabled():
|
||||
metadata={},
|
||||
)
|
||||
|
||||
with patch("nanobot.cli.commands._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
handled = await commands._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
with patch("nanobot.cli.terminal._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
handled = await terminal._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
|
||||
assert handled is True
|
||||
assert calls == ["I should search first."]
|
||||
@@ -79,10 +79,10 @@ async def test_reasoning_delta_buffers_until_sentence_boundary():
|
||||
channels_config = SimpleNamespace(
|
||||
send_progress=True, send_tool_hints=False, show_reasoning=True,
|
||||
)
|
||||
reasoning_buffer = commands._ReasoningBuffer()
|
||||
reasoning_buffer = terminal._ReasoningBuffer()
|
||||
|
||||
with patch("nanobot.cli.commands._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
first = await commands._maybe_print_interactive_progress(
|
||||
with patch("nanobot.cli.terminal._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
first = await terminal._maybe_print_interactive_progress(
|
||||
SimpleNamespace(
|
||||
content="The",
|
||||
event=ProgressEvent(content="The", reasoning_delta=True),
|
||||
@@ -92,7 +92,7 @@ async def test_reasoning_delta_buffers_until_sentence_boundary():
|
||||
channels_config,
|
||||
reasoning_buffer=reasoning_buffer,
|
||||
)
|
||||
second = await commands._maybe_print_interactive_progress(
|
||||
second = await terminal._maybe_print_interactive_progress(
|
||||
SimpleNamespace(
|
||||
content=" user asked.",
|
||||
event=ProgressEvent(content=" user asked.", reasoning_delta=True),
|
||||
@@ -114,10 +114,10 @@ async def test_reasoning_end_flushes_buffered_delta():
|
||||
channels_config = SimpleNamespace(
|
||||
send_progress=True, send_tool_hints=False, show_reasoning=True,
|
||||
)
|
||||
reasoning_buffer = commands._ReasoningBuffer()
|
||||
reasoning_buffer = terminal._ReasoningBuffer()
|
||||
|
||||
with patch("nanobot.cli.commands._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
delta = await commands._maybe_print_interactive_progress(
|
||||
with patch("nanobot.cli.terminal._print_cli_reasoning", side_effect=lambda t, th, r=None: calls.append(t)):
|
||||
delta = await terminal._maybe_print_interactive_progress(
|
||||
SimpleNamespace(
|
||||
content="The user asked",
|
||||
event=ProgressEvent(content="The user asked", reasoning_delta=True),
|
||||
@@ -127,7 +127,7 @@ async def test_reasoning_end_flushes_buffered_delta():
|
||||
channels_config,
|
||||
reasoning_buffer=reasoning_buffer,
|
||||
)
|
||||
end = await commands._maybe_print_interactive_progress(
|
||||
end = await terminal._maybe_print_interactive_progress(
|
||||
SimpleNamespace(
|
||||
content="",
|
||||
event=ProgressEvent(reasoning_end=True),
|
||||
@@ -155,8 +155,8 @@ async def test_reasoning_hidden_when_show_reasoning_disabled():
|
||||
metadata={},
|
||||
)
|
||||
|
||||
with patch("nanobot.cli.commands._print_cli_reasoning") as mock_reasoning:
|
||||
handled = await commands._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
with patch("nanobot.cli.terminal._print_cli_reasoning") as mock_reasoning:
|
||||
handled = await terminal._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
|
||||
assert handled is True
|
||||
mock_reasoning.assert_not_called()
|
||||
@@ -178,8 +178,8 @@ async def test_non_reasoning_progress_not_affected_by_show_reasoning():
|
||||
async def fake_print(text: str, thinking=None, renderer=None):
|
||||
calls.append(text)
|
||||
|
||||
with patch("nanobot.cli.commands._print_interactive_progress_line", side_effect=fake_print):
|
||||
handled = await commands._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
with patch("nanobot.cli.terminal._print_interactive_progress_line", side_effect=fake_print):
|
||||
handled = await terminal._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
|
||||
assert handled is True
|
||||
assert calls == ["working on it..."]
|
||||
@@ -200,10 +200,10 @@ async def test_reasoning_shown_when_send_progress_disabled():
|
||||
)
|
||||
|
||||
with patch(
|
||||
"nanobot.cli.commands._print_cli_reasoning",
|
||||
"nanobot.cli.terminal._print_cli_reasoning",
|
||||
side_effect=lambda t, th, r=None: calls.append(t),
|
||||
):
|
||||
handled = await commands._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
handled = await terminal._maybe_print_interactive_progress(msg, None, channels_config)
|
||||
|
||||
assert handled is True
|
||||
assert calls == ["Let me think about this..."]
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
Surrogate characters in CLI input must not crash history file writes.
|
||||
"""
|
||||
|
||||
from nanobot.cli.commands import SafeFileHistory, _sanitize_surrogates
|
||||
from nanobot.cli.commands import SafeFileHistory as LegacySafeFileHistory
|
||||
from nanobot.cli.terminal import SafeFileHistory, _sanitize_surrogates
|
||||
|
||||
|
||||
def test_commands_keeps_safe_file_history_import_compatible() -> None:
|
||||
assert LegacySafeFileHistory is SafeFileHistory
|
||||
|
||||
|
||||
class TestSanitizeSurrogates:
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestMidTurnCommandDispatchedDirectly:
|
||||
))
|
||||
loop.sessions.save = MagicMock()
|
||||
loop.sessions.invalidate = MagicMock()
|
||||
loop._schedule_background = MagicMock()
|
||||
loop.schedule_background = MagicMock()
|
||||
loop._cancel_active_tasks = AsyncMock(return_value=0)
|
||||
return loop
|
||||
|
||||
|
||||
Reference in New Issue
Block a user