mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 05:12:26 +00:00
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>
218 lines
7.4 KiB
Python
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
|