Files
OpenJarvis/tests/cli/test_memory_cmd.py
T
Elliot SluskyandGitHub 433d10db5e feat(memory): native persistent memory service integrated into core (#579)
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.
2026-06-22 13:59:02 -07:00

176 lines
5.4 KiB
Python

"""Tests for ``jarvis memory`` CLI commands."""
from __future__ import annotations
import importlib
from pathlib import Path
from click.testing import CliRunner
from openjarvis.cli import cli
from openjarvis.core.registry import MemoryRegistry
from openjarvis.memory.store import LocalFactStore
from openjarvis.tools.storage.sqlite import SQLiteMemory
def _register_sqlite():
"""Re-register sqlite backend (conftest clears registries)."""
if not MemoryRegistry.contains("sqlite"):
MemoryRegistry.register_value("sqlite", SQLiteMemory)
def test_memory_index_file(tmp_path: Path, monkeypatch):
"""Index a single text file and check success message."""
_register_sqlite()
db_path = str(tmp_path / "mem.db")
# Create a text file with enough content
doc = tmp_path / "doc.txt"
doc.write_text(" ".join(f"word{i}" for i in range(100)))
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(cli, ["memory", "index", str(doc)])
assert result.exit_code == 0
assert "Indexed" in result.output or "chunk" in result.output
def test_memory_index_nonexistent(tmp_path: Path):
"""Indexing a nonexistent path should fail."""
_register_sqlite()
result = CliRunner().invoke(cli, ["memory", "index", str(tmp_path / "nope")])
assert result.exit_code != 0
def test_memory_search_returns_results(tmp_path: Path, monkeypatch):
"""Search returns results from pre-populated backend."""
_register_sqlite()
db_path = str(tmp_path / "mem.db")
backend = SQLiteMemory(db_path=db_path)
backend.store(
"Python programming language guide",
source="guide.md",
)
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(cli, ["memory", "search", "Python"])
assert result.exit_code == 0
assert "Python" in result.output
backend.close()
def test_memory_search_no_results(tmp_path: Path, monkeypatch):
"""Search with no matches shows appropriate message."""
_register_sqlite()
db_path = str(tmp_path / "mem.db")
backend = SQLiteMemory(db_path=db_path)
backend.store("some unrelated content about cats")
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(cli, ["memory", "search", "quantum supercollider"])
assert result.exit_code == 0
assert "No results" in result.output
backend.close()
def test_memory_stats_shows_count(tmp_path: Path, monkeypatch):
"""Stats command shows document count."""
_register_sqlite()
db_path = str(tmp_path / "mem.db")
backend = SQLiteMemory(db_path=db_path)
backend.store("doc one")
backend.store("doc two")
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(cli, ["memory", "stats"])
assert result.exit_code == 0
assert "2" in result.output
backend.close()
def _patch_fact_store(monkeypatch, tmp_path: Path) -> LocalFactStore:
"""Point ``jarvis memory list/clear`` at a temp fact store."""
mod = importlib.import_module("openjarvis.cli.memory_cmd")
store = LocalFactStore(tmp_path / "facts.jsonl")
monkeypatch.setattr(mod, "_get_fact_store", lambda: store)
return store
def test_memory_list_empty(tmp_path: Path, monkeypatch):
_patch_fact_store(monkeypatch, tmp_path)
result = CliRunner().invoke(cli, ["memory", "list"])
assert result.exit_code == 0
assert "No memory facts" in result.output
def test_memory_list_shows_facts(tmp_path: Path, monkeypatch):
store = _patch_fact_store(monkeypatch, tmp_path)
store.add("User prefers dark mode")
store.add("User lives in Berlin")
result = CliRunner().invoke(cli, ["memory", "list"])
assert result.exit_code == 0
assert "dark mode" in result.output
assert "Berlin" in result.output
def test_memory_clear_with_confirmation(tmp_path: Path, monkeypatch):
store = _patch_fact_store(monkeypatch, tmp_path)
store.add("fact one")
store.add("fact two")
result = CliRunner().invoke(cli, ["memory", "clear"], input="y\n")
assert result.exit_code == 0
assert "Cleared 2" in result.output
assert store.count() == 0
def test_memory_clear_aborted(tmp_path: Path, monkeypatch):
store = _patch_fact_store(monkeypatch, tmp_path)
store.add("keep me")
result = CliRunner().invoke(cli, ["memory", "clear"], input="n\n")
assert result.exit_code == 0
assert "Aborted" in result.output
assert store.count() == 1
def test_memory_clear_yes_flag(tmp_path: Path, monkeypatch):
store = _patch_fact_store(monkeypatch, tmp_path)
store.add("fact")
result = CliRunner().invoke(cli, ["memory", "clear", "--yes"])
assert result.exit_code == 0
assert "Cleared 1" in result.output
assert store.count() == 0
def test_memory_clear_empty(tmp_path: Path, monkeypatch):
_patch_fact_store(monkeypatch, tmp_path)
result = CliRunner().invoke(cli, ["memory", "clear"])
assert result.exit_code == 0
assert "No memory facts to clear" in result.output