mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 05:12:26 +00:00
Adds the openjarvis.memory package (LocalFactStore, FactExtractor, background MemoryService), starts/stops it in the jarvis serve and jarvis chat lifecycle, feeds completed non-streaming exchanges to it, adds [memory] config support, and adds jarvis memory list/clear CLI commands. Extraction runs on a background thread and degrades to a no-op on any failure (BrokenPipe, timeouts, unparseable output) so it can never block a reply or crash the host. Disabled by default. Closes #393, #571, #572, #573.
196 lines
6.7 KiB
Python
196 lines
6.7 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.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 the memory service, submits each turn, and stops it."""
|
|
|
|
class _SpyMemoryService:
|
|
def __init__(self) -> None:
|
|
self.started = False
|
|
self.stopped = False
|
|
self.submissions: list[tuple[str, str]] = []
|
|
|
|
def start(self) -> None:
|
|
self.started = True
|
|
|
|
def submit(self, user_text: str, assistant_text: str = "") -> bool:
|
|
self.submissions.append((user_text, assistant_text))
|
|
return True
|
|
|
|
def stop(self, timeout: float = 2.0) -> None:
|
|
self.stopped = True
|
|
|
|
spy = _SpyMemoryService()
|
|
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",
|
|
return_value=spy,
|
|
),
|
|
):
|
|
result = CliRunner().invoke(
|
|
chat,
|
|
["--agent", "simple_chat_agent", "--model", "test-model"],
|
|
input="hello\n/quit\n",
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
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
|