Files
OpenJarvis/tests/cli/test_chat_cmd.py
T
Elliot Slusky c3a7ffebff fix(memory): recall auto-captured facts across sessions (#740)
* fix(memory): recall auto-captured facts

* fix(memory): harden recalled context injection

* fix(memory): preserve mixed context history

* fix(agents): preserve caller system context
2026-08-13 17:07:10 -07:00

292 lines
10 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.memory.store import LocalFactStore
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_direct_chat_injects_auto_memory_facts(self, tmp_path) -> None:
facts_path = tmp_path / "facts.jsonl"
LocalFactStore(facts_path).add(
"The user's favorite color is blue",
source="auto",
)
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = {"content": "Blue."}
config = JarvisConfig()
config.intelligence.default_model = "test-model"
config.memory.enabled = True
config.memory.facts_path = str(facts_path)
config.agent.context_from_memory = True
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", return_value=None),
patch("openjarvis.cli.ask._get_memory_backend", return_value=None),
):
result = CliRunner().invoke(
chat,
["--model", "test-model"],
input="What is my favorite color?\n/quit\n",
)
assert result.exit_code == 0
messages = engine.generate.call_args.args[0]
assert messages[0].role.value == "system"
assert "favorite color is blue" in messages[0].content
def test_chat_generation_survives_fact_store_failure(self) -> None:
class _FailingMemoryService:
def start(self) -> None:
pass
def stop(self, timeout: float = 2.0) -> None:
pass
def list_facts(self):
raise OSError("fact store unavailable")
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = {"content": "Still working."}
config = JarvisConfig()
config.intelligence.default_model = "test-model"
config.memory.enabled = True
config.agent.context_from_memory = True
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",
return_value=_FailingMemoryService(),
),
patch("openjarvis.cli.ask._get_memory_backend", return_value=None),
):
result = CliRunner().invoke(
chat,
["--model", "test-model"],
input="hello\n/quit\n",
)
assert result.exit_code == 0
assert "Still working." in result.output
engine.generate.assert_called_once()
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