nanobot/tests/command/test_router_dispatchable.py
Xubin Ren c450d6fd3f fix(config): make model preset switching atomic
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00

146 lines
5.5 KiB
Python

"""Tests for CommandRouter.is_dispatchable_command and mid-turn command interception."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from nanobot.command.builtin import register_builtin_commands
from nanobot.command.router import CommandContext, CommandRouter
class TestIsDispatchableCommand:
"""Unit tests for the is_dispatchable_command() predicate."""
@pytest.fixture()
def router(self) -> CommandRouter:
r = CommandRouter()
register_builtin_commands(r)
return r
def test_exact_commands_match(self, router: CommandRouter) -> None:
assert router.is_dispatchable_command("/new")
assert router.is_dispatchable_command("/help")
assert router.is_dispatchable_command("/model")
assert router.is_dispatchable_command("/dream")
assert router.is_dispatchable_command("/dream-log")
assert router.is_dispatchable_command("/dream-restore")
def test_prefix_commands_match(self, router: CommandRouter) -> None:
assert router.is_dispatchable_command("/dream-log abc123")
assert router.is_dispatchable_command("/dream-restore def456")
assert router.is_dispatchable_command("/model fast")
def test_priority_commands_not_matched(self, router: CommandRouter) -> None:
# Priority commands are NOT in the dispatchable tiers — they are
# handled by is_priority() separately.
assert not router.is_dispatchable_command("/stop")
assert not router.is_dispatchable_command("/restart")
def test_regular_text_not_matched(self, router: CommandRouter) -> None:
assert not router.is_dispatchable_command("hello")
assert not router.is_dispatchable_command("what is 2+2?")
assert not router.is_dispatchable_command("")
def test_case_insensitive(self, router: CommandRouter) -> None:
assert router.is_dispatchable_command("/NEW")
assert router.is_dispatchable_command("/Help")
def test_strips_whitespace(self, router: CommandRouter) -> None:
assert router.is_dispatchable_command(" /new ")
def test_unknown_slash_command_not_matched(self, router: CommandRouter) -> None:
assert not router.is_dispatchable_command("/unknown")
assert not router.is_dispatchable_command("/foo bar")
class TestMidTurnCommandDispatchedDirectly:
"""Verify that commands matching is_dispatchable_command() are dispatched
correctly when session=None (the mid-turn path)."""
@pytest.fixture()
def router(self) -> CommandRouter:
r = CommandRouter()
register_builtin_commands(r)
return r
@pytest.fixture()
def fake_loop(self) -> MagicMock:
loop = MagicMock()
loop.sessions = MagicMock()
loop.sessions.get_or_create = MagicMock(return_value=MagicMock(
messages=[], last_consolidated=0, clear=MagicMock(),
))
loop.sessions.save = MagicMock()
loop.sessions.invalidate = MagicMock()
loop._schedule_background = MagicMock()
loop._cancel_active_tasks = AsyncMock(return_value=0)
return loop
@pytest.fixture()
def fake_msg(self) -> MagicMock:
msg = MagicMock()
msg.channel = "test"
msg.chat_id = "chat1"
msg.content = "/new"
msg.metadata = {}
return msg
@pytest.mark.asyncio
async def test_new_dispatched_with_session_none(
self, router: CommandRouter, fake_loop: MagicMock, fake_msg: MagicMock,
) -> None:
"""cmd_new works when session=None (mid-turn dispatch path)."""
ctx = CommandContext(
msg=fake_msg, session=None,
key="test:chat1", raw="/new", loop=fake_loop,
)
result = await router.dispatch(ctx)
assert result is not None
assert "New session" in result.content
fake_loop.sessions.get_or_create.assert_called_once_with("test:chat1")
@pytest.mark.asyncio
async def test_help_dispatched_with_session_none(
self, router: CommandRouter, fake_loop: MagicMock, fake_msg: MagicMock,
) -> None:
ctx = CommandContext(
msg=fake_msg, session=None,
key="test:chat1", raw="/help", loop=fake_loop,
)
result = await router.dispatch(ctx)
assert result is not None
@pytest.mark.asyncio
async def test_prefix_command_args_populated(self, router: CommandRouter) -> None:
"""Prefix commands have args populated correctly in mid-turn path."""
# Use a custom prefix handler to avoid needing full mock setup.
custom = CommandRouter()
captured_args = []
async def fake_handler(ctx: CommandContext) -> None:
captured_args.append(ctx.args)
return None
custom.prefix("/test ", fake_handler)
ctx = CommandContext(
msg=MagicMock(channel="test", chat_id="c1", metadata={}),
session=None, key="test:c1", raw="/test hello world", loop=MagicMock(),
)
await custom.dispatch(ctx)
assert captured_args == ["hello world"]
@pytest.mark.asyncio
async def test_non_command_returns_none(
self, router: CommandRouter, fake_loop: MagicMock, fake_msg: MagicMock,
) -> None:
"""Regular text returns None from dispatch (not a command)."""
ctx = CommandContext(
msg=fake_msg, session=None,
key="test:chat1", raw="hello world", loop=fake_loop,
)
result = await router.dispatch(ctx)
assert result is None