Files
OpenJarvis/tests/cli/test_chat_cmd.py
T
eb2b612c7c fix(memory): address service follow-ups (#591)
Closes #582. Route fact-store construction through a new FactStoreRegistry (local backend registered by default); align the default facts path with get_config_dir(); wire completed chat exchanges (streamed and non-streamed) through the EventBus so the memory service captures them consistently; reload the local fact store from disk before operations so external clears don't resurrect stale facts; make the affected config/persona/memory/CLI/route tests hermetic; and refresh uv.lock with the current resolver (locks pytest-xdist + transitive deps, drops py3.14 artifacts since the project constrains Python <3.14).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:58:49 -07:00

218 lines
7.4 KiB
Python

"""Tests for ``jarvis chat`` interactive REPL command."""
from __future__ import annotations
from unittest import mock
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
from openjarvis.agents._stubs import (
AgentContext,
AgentResult,
BaseAgent,
ToolUsingAgent,
)
from openjarvis.cli.chat_cmd import _read_input, chat
from openjarvis.core.config import JarvisConfig
from openjarvis.core.events import Event, EventBus, EventType
from openjarvis.core.registry import AgentRegistry, ToolRegistry
from openjarvis.core.types import ToolCall, ToolResult
from openjarvis.tools._stubs import BaseTool, ToolSpec
class _SimpleChatAgent(BaseAgent):
agent_id = "simple_chat_agent"
def run(self, input, context: AgentContext | None = None, **kwargs):
return AgentResult(content="simple ok", turns=1)
class _DangerousChatTool(BaseTool):
tool_id = "dangerous_chat"
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="dangerous_chat",
description="Confirmation-gated chat tool.",
requires_confirmation=True,
)
def execute(self, **params) -> ToolResult:
return ToolResult(
tool_name="dangerous_chat",
content="chat executed!",
success=True,
)
class _ToolChatAgent(ToolUsingAgent):
agent_id = "tool_chat_agent"
def run(self, input, context: AgentContext | None = None, **kwargs):
result = self._executor.execute(
ToolCall(id="chat", name="dangerous_chat", arguments="{}")
)
return AgentResult(content=result.content, tool_results=[result], turns=1)
class TestChatCommand:
"""Test the Click command definition and help output."""
def test_command_exists(self) -> None:
result = CliRunner().invoke(chat, ["--help"])
assert result.exit_code == 0
assert "interactive" in result.output.lower() or "chat" in result.output.lower()
def test_options(self) -> None:
result = CliRunner().invoke(chat, ["--help"])
assert result.exit_code == 0
assert "--engine" in result.output
assert "--model" in result.output
assert "--agent" in result.output
assert "--tools" in result.output
assert "--system" in result.output
def test_slash_commands_listed(self) -> None:
result = CliRunner().invoke(chat, ["--help"])
assert result.exit_code == 0
assert "/quit" in result.output
class TestReadInput:
"""Test the _read_input helper function."""
def test_read_input_eof(self) -> None:
with mock.patch("builtins.input", side_effect=EOFError):
assert _read_input() is None
def test_read_input_keyboard_interrupt(self) -> None:
with mock.patch("builtins.input", side_effect=KeyboardInterrupt):
assert _read_input() is None
def test_read_input_normal(self) -> None:
with mock.patch("builtins.input", return_value="hello"):
assert _read_input() == "hello"
class TestChatAgents:
def test_simple_agent_does_not_receive_tool_only_kwargs(self) -> None:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = {"content": "engine fallback"}
config = JarvisConfig()
config.intelligence.default_model = "test-model"
AgentRegistry.register_value("simple_chat_agent", _SimpleChatAgent)
with (
patch("openjarvis.cli.chat_cmd.load_config", return_value=config),
patch("openjarvis.engine.get_engine", return_value=("mock", engine)),
patch("openjarvis.intelligence.register_builtin_models"),
):
result = CliRunner().invoke(
chat,
["--agent", "simple_chat_agent", "--model", "test-model"],
input="hello\n/quit\n",
)
assert result.exit_code == 0
assert "simple ok" in result.output
assert "failed" not in result.output.lower()
def test_memory_service_started_fed_and_stopped(self) -> None:
"""The REPL starts memory, publishes each turn, and stops it."""
class _SpyMemoryService:
def __init__(self, bus: EventBus) -> None:
self.bus = bus
self.started = False
self.stopped = False
self.submissions: list[tuple[str, str]] = []
def start(self) -> None:
self.started = True
self.bus.subscribe(
EventType.CHAT_EXCHANGE_COMPLETED,
self._on_completed_exchange,
)
def _on_completed_exchange(self, event: Event) -> None:
self.submissions.append(
(
event.data["user_text"],
event.data.get("assistant_text", ""),
)
)
def stop(self, timeout: float = 2.0) -> None:
self.stopped = True
self.bus.unsubscribe(
EventType.CHAT_EXCHANGE_COMPLETED,
self._on_completed_exchange,
)
spy: _SpyMemoryService | None = None
def _build_memory_service(*args, event_bus: EventBus | None = None, **kwargs):
nonlocal spy
assert event_bus is not None
spy = _SpyMemoryService(event_bus)
return spy
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = {"content": "engine fallback"}
config = JarvisConfig()
config.intelligence.default_model = "test-model"
AgentRegistry.register_value("simple_chat_agent", _SimpleChatAgent)
with (
patch("openjarvis.cli.chat_cmd.load_config", return_value=config),
patch("openjarvis.engine.get_engine", return_value=("mock", engine)),
patch("openjarvis.intelligence.register_builtin_models"),
patch(
"openjarvis.memory.build_memory_service",
side_effect=_build_memory_service,
),
):
result = CliRunner().invoke(
chat,
["--agent", "simple_chat_agent", "--model", "test-model"],
input="hello\n/quit\n",
)
assert result.exit_code == 0
assert spy is not None
assert spy.started is True
assert spy.stopped is True
assert spy.submissions == [("hello", "simple ok")]
def test_tool_agent_uses_legacy_agent_tools_and_prompts_confirmation(self) -> None:
engine = MagicMock()
engine.engine_id = "mock"
config = JarvisConfig()
config.intelligence.default_model = "test-model"
config.agent.tools = "dangerous_chat"
config.agent.max_turns = 3
AgentRegistry.register_value("tool_chat_agent", _ToolChatAgent)
ToolRegistry.register_value("dangerous_chat", _DangerousChatTool)
with (
patch("openjarvis.cli.chat_cmd.load_config", return_value=config),
patch("openjarvis.engine.get_engine", return_value=("mock", engine)),
patch("openjarvis.intelligence.register_builtin_models"),
):
result = CliRunner().invoke(
chat,
["--agent", "tool_chat_agent", "--model", "test-model"],
input="run tool\ny\n/quit\n",
)
assert result.exit_code == 0
assert "Confirm:" in result.output
assert "chat executed!" in result.output