mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 05:12:26 +00:00
Memory tools degraded silently and misleadingly when the mandatory
`openjarvis_rust` extension was absent from the *serving* venv:
- `POST /v1/memory/store` returned HTTP 200 `{"status":"stored","note":
"no backend available"}` and stored nothing (silent data loss).
- `POST /v1/memory/index` returned a generic "No memory backend available",
and the desktop frontend discarded the server `detail` and threw a blanket
"Failed to index path", blaming the path instead of the real cause.
- `GET /v1/memory/config` reported `backend_type: sqlite` even though no
backend could be constructed.
Root cause: `SQLiteMemory.__init__` calls `get_rust_module()` (which raises
ImportError by design — the Rust ext is mandatory, no Python fallback), and
`_get_memory_backend` swallowed that ImportError and returned `None`,
conflating "native extension missing" (a hard install error) with "memory
intentionally disabled" (benign). A chunking floor also silently dropped whole
short documents, and the installer never verified the extension imported from
the serving venv before writing its success marker.
Fix (no fake Python fallback — the Rust ext stays mandatory by design):
- Add `MemoryBackendUnavailable` + `RUST_MISSING_HINT` in tools/storage/_stubs.
`SQLiteMemory.__init__` translates the bridge ImportError into this clear,
actionable error ("run `uv run maturin develop ...`").
- `_get_memory_backend` distinguishes the two cases: a missing native ext
raises HTTP 503 with the actionable hint; a benign unconfigured backend
still returns `None` (graceful path preserved for search/stats).
- `/store` now returns 503 instead of a 200 silent no-op.
- `/config` reports `available: false` + `detail` instead of falsely claiming
a healthy `backend_type`.
- `/index` adds a `note` when `chunks_indexed == 0` so "indexed" never
silently means "stored nothing".
- chunk_text no longer drops an entire short document below `min_chunk_size`
(the floor only discards tiny *trailing* fragments now).
- Frontend `storeMemory`/`indexMemoryPath` surface the server `detail` instead
of blanket strings; `MemoryConfig` gains optional `available`/`detail`.
(Left the pre-existing `backend` vs `backend_type` mismatch untouched.)
- build-extension.sh verifies `import openjarvis_rust` succeeds in the serving
venv before writing the `extension-built` marker.
Regression tests: tests/server/test_api_routes.py::TestMemoryRustMissing mocks
`get_rust_module` to raise ImportError and asserts /store (503, not 200 no-op),
/index (actionable detail, not "Failed to index path"), and /config
(available:false) all surface the clear error; tests/memory/test_chunking.py
asserts short-only docs are kept while tiny trailing fragments are still
filtered.
Fixes #502
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
4.8 KiB
Python
144 lines
4.8 KiB
Python
"""Tests for extended API routes."""
|
|
|
|
import pytest
|
|
|
|
fastapi = pytest.importorskip("fastapi")
|
|
from fastapi import FastAPI # noqa: E402
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|
|
|
from openjarvis.server.api_routes import include_all_routes # noqa: E402
|
|
|
|
|
|
def _make_app():
|
|
app = FastAPI()
|
|
include_all_routes(app)
|
|
return app
|
|
|
|
|
|
class TestAgentRoutes:
|
|
def test_list_agents(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/agents")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "registered" in data
|
|
assert "running" in data
|
|
|
|
def test_create_agent(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.post("/v1/agents", json={"agent_type": "simple"})
|
|
# May succeed or fail depending on agent_tools availability
|
|
assert resp.status_code in (200, 501)
|
|
|
|
def test_kill_nonexistent(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.delete("/v1/agents/nonexistent")
|
|
assert resp.status_code in (404, 501)
|
|
|
|
|
|
class TestMemoryRoutes:
|
|
def test_search(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.post("/v1/memory/search", json={"query": "test"})
|
|
# May fail if SQLite not set up, that's ok
|
|
assert resp.status_code in (200, 500)
|
|
|
|
def test_stats(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/memory/stats")
|
|
assert resp.status_code in (200, 500)
|
|
|
|
|
|
class TestMemoryRustMissing:
|
|
"""Regression for #502: when the native ``openjarvis_rust`` extension is
|
|
missing from the serving venv, memory ops must surface a CLEAR, ACTIONABLE
|
|
error — never the misleading "Failed to index path" or a 200 silent no-op.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _client(monkeypatch):
|
|
# Force the same failure mode as a venv without the compiled extension.
|
|
def _boom():
|
|
raise ImportError("No module named 'openjarvis_rust'")
|
|
|
|
import openjarvis._rust_bridge as bridge
|
|
|
|
monkeypatch.setattr(bridge, "get_rust_module", _boom)
|
|
return TestClient(_make_app())
|
|
|
|
def test_store_is_not_a_silent_noop(self, monkeypatch):
|
|
client = self._client(monkeypatch)
|
|
resp = client.post("/v1/memory/store", json={"content": "hi"})
|
|
# Must NOT return the old 200 {"status":"stored","note":"no backend..."}.
|
|
assert resp.status_code == 503
|
|
detail = resp.json()["detail"]
|
|
assert "openjarvis_rust" in detail
|
|
assert "maturin develop" in detail
|
|
|
|
def test_index_surfaces_actionable_detail(self, monkeypatch, tmp_path):
|
|
(tmp_path / "note.txt").write_text("hello world some content here")
|
|
client = self._client(monkeypatch)
|
|
resp = client.post("/v1/memory/index", json={"path": str(tmp_path)})
|
|
assert resp.status_code == 503
|
|
detail = resp.json()["detail"]
|
|
# The frontend reads this `detail`; it must point at the real cause,
|
|
# not blame the indexed path.
|
|
assert "openjarvis_rust" in detail
|
|
assert detail != "Failed to index path"
|
|
assert detail != "No memory backend available"
|
|
|
|
def test_config_reports_unavailable(self, monkeypatch):
|
|
client = self._client(monkeypatch)
|
|
resp = client.get("/v1/memory/config")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
# Must not falsely report a healthy backend when none could be built.
|
|
assert data["available"] is False
|
|
assert "openjarvis_rust" in (data["detail"] or "")
|
|
|
|
|
|
class TestBudgetRoutes:
|
|
def test_get_budget(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/budget")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "limits" in data
|
|
assert "usage" in data
|
|
|
|
def test_set_limits(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.put("/v1/budget/limits", json={"max_tokens_per_day": 100000})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["limits"]["max_tokens_per_day"] == 100000
|
|
|
|
|
|
class TestMetricsRoute:
|
|
def test_metrics_endpoint(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/metrics")
|
|
assert resp.status_code == 200
|
|
assert "openjarvis" in resp.text or "No metrics" in resp.text
|
|
|
|
|
|
class TestSkillRoutes:
|
|
def test_list_skills(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/skills")
|
|
assert resp.status_code == 200
|
|
assert "skills" in resp.json()
|
|
|
|
|
|
class TestSessionRoutes:
|
|
def test_list_sessions(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/sessions")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
class TestTraceRoutes:
|
|
def test_list_traces(self):
|
|
client = TestClient(_make_app())
|
|
resp = client.get("/v1/traces")
|
|
assert resp.status_code == 200
|