Add persona and language command localization
This commit is contained in:
@@ -71,3 +71,29 @@ def test_runtime_context_is_separate_untrusted_user_message(tmp_path) -> None:
|
||||
assert "Channel: cli" in user_content
|
||||
assert "Chat ID: direct" in user_content
|
||||
assert "Return exactly: OK" in user_content
|
||||
|
||||
|
||||
def test_persona_prompt_uses_persona_overrides_and_memory(tmp_path: Path) -> None:
|
||||
workspace = _make_workspace(tmp_path)
|
||||
(workspace / "AGENTS.md").write_text("root agents", encoding="utf-8")
|
||||
(workspace / "SOUL.md").write_text("root soul", encoding="utf-8")
|
||||
(workspace / "USER.md").write_text("root user", encoding="utf-8")
|
||||
(workspace / "memory").mkdir()
|
||||
(workspace / "memory" / "MEMORY.md").write_text("root memory", encoding="utf-8")
|
||||
|
||||
persona_dir = workspace / "personas" / "coder"
|
||||
persona_dir.mkdir(parents=True)
|
||||
(persona_dir / "SOUL.md").write_text("coder soul", encoding="utf-8")
|
||||
(persona_dir / "USER.md").write_text("coder user", encoding="utf-8")
|
||||
(persona_dir / "memory").mkdir()
|
||||
(persona_dir / "memory" / "MEMORY.md").write_text("coder memory", encoding="utf-8")
|
||||
|
||||
builder = ContextBuilder(workspace)
|
||||
prompt = builder.build_system_prompt(persona="coder")
|
||||
|
||||
assert "Current persona: coder" in prompt
|
||||
assert "root agents" in prompt
|
||||
assert "coder soul" in prompt
|
||||
assert "coder user" in prompt
|
||||
assert "coder memory" in prompt
|
||||
assert "root memory" not in prompt
|
||||
|
||||
138
tests/test_persona_commands.py
Normal file
138
tests/test_persona_commands.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Tests for session-scoped persona switching."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
|
||||
def _make_loop(workspace: Path, provider: MagicMock | None = None):
|
||||
"""Create an AgentLoop with a real workspace and lightweight mocks."""
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.queue import MessageBus
|
||||
|
||||
bus = MessageBus()
|
||||
provider = provider or MagicMock()
|
||||
provider.get_default_model.return_value = "test-model"
|
||||
|
||||
with patch("nanobot.agent.loop.SubagentManager"):
|
||||
loop = AgentLoop(bus=bus, provider=provider, workspace=workspace)
|
||||
return loop, provider
|
||||
|
||||
|
||||
def _make_persona(workspace: Path, name: str, soul: str) -> None:
|
||||
persona_dir = workspace / "personas" / name
|
||||
persona_dir.mkdir(parents=True)
|
||||
(persona_dir / "SOUL.md").write_text(soul, encoding="utf-8")
|
||||
|
||||
|
||||
class TestPersonaCommands:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persona_switch_clears_session_and_persists_selection(self, tmp_path: Path) -> None:
|
||||
_make_persona(tmp_path, "coder", "You are coder persona.")
|
||||
loop, _provider = _make_loop(tmp_path)
|
||||
loop.memory_consolidator.archive_unconsolidated = AsyncMock(return_value=True)
|
||||
|
||||
session = loop.sessions.get_or_create("cli:direct")
|
||||
session.add_message("user", "hello")
|
||||
session.add_message("assistant", "hi")
|
||||
loop.sessions.save(session)
|
||||
|
||||
response = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/persona set coder")
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.content == "Switched persona to coder. New session started."
|
||||
loop.memory_consolidator.archive_unconsolidated.assert_awaited_once()
|
||||
|
||||
switched = loop.sessions.get_or_create("cli:direct")
|
||||
assert switched.metadata["persona"] == "coder"
|
||||
assert switched.messages == []
|
||||
|
||||
current = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/persona current")
|
||||
)
|
||||
listing = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/persona list")
|
||||
)
|
||||
|
||||
assert current is not None
|
||||
assert current.content == "Current persona: coder"
|
||||
assert listing is not None
|
||||
assert "- default" in listing.content
|
||||
assert "- coder (current)" in listing.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_includes_persona_commands(self, tmp_path: Path) -> None:
|
||||
loop, _provider = _make_loop(tmp_path)
|
||||
|
||||
response = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/help")
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert "/persona current" in response.content
|
||||
assert "/persona set <name>" in response.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_language_switch_localizes_help(self, tmp_path: Path) -> None:
|
||||
loop, _provider = _make_loop(tmp_path)
|
||||
|
||||
switched = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/lang set zh")
|
||||
)
|
||||
help_response = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/help")
|
||||
)
|
||||
|
||||
assert switched is not None
|
||||
assert "已切换语言为" in switched.content
|
||||
assert help_response is not None
|
||||
assert "/lang current — 查看当前语言" in help_response.content
|
||||
assert "/persona current — 查看当前人格" in help_response.content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_active_persona_changes_prompt_memory_scope(self, tmp_path: Path) -> None:
|
||||
provider = MagicMock()
|
||||
provider.get_default_model.return_value = "test-model"
|
||||
provider.chat_with_retry = AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
has_tool_calls=False,
|
||||
content="ok",
|
||||
finish_reason="stop",
|
||||
reasoning_content=None,
|
||||
thinking_blocks=None,
|
||||
)
|
||||
)
|
||||
|
||||
(tmp_path / "SOUL.md").write_text("root soul", encoding="utf-8")
|
||||
persona_dir = tmp_path / "personas" / "coder"
|
||||
persona_dir.mkdir(parents=True)
|
||||
(persona_dir / "SOUL.md").write_text("coder soul", encoding="utf-8")
|
||||
(persona_dir / "memory").mkdir()
|
||||
(persona_dir / "memory" / "MEMORY.md").write_text("coder memory", encoding="utf-8")
|
||||
|
||||
loop, provider = _make_loop(tmp_path, provider)
|
||||
session = loop.sessions.get_or_create("cli:direct")
|
||||
session.metadata["persona"] = "coder"
|
||||
loop.sessions.save(session)
|
||||
|
||||
response = await loop._process_message(
|
||||
InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="hello")
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.content == "ok"
|
||||
|
||||
messages = provider.chat_with_retry.await_args.kwargs["messages"]
|
||||
assert "Current persona: coder" in messages[0]["content"]
|
||||
assert "coder soul" in messages[0]["content"]
|
||||
assert "coder memory" in messages[0]["content"]
|
||||
assert "root soul" not in messages[0]["content"]
|
||||
Reference in New Issue
Block a user