mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +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.
176 lines
5.4 KiB
Python
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
|