mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 05:12:26 +00:00
- openjarvis.__version__ now comes from importlib.metadata.version("openjarvis")
instead of a hardcoded string, so it stays in sync with pyproject.toml on every
release. Tests assert against openjarvis.__version__ rather than a literal.
- Bump actions/checkout@v4 → @v6 and astral-sh/setup-uv@v4 → @v8 across every
workflow ahead of the 2026-06-02 Node.js 20 deprecation on GitHub Actions runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
298 lines
9.4 KiB
Python
298 lines
9.4 KiB
Python
"""Tests for the Python SDK — Jarvis class and MemoryHandle."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import openjarvis
|
|
from openjarvis.core.config import JarvisConfig
|
|
from openjarvis.sdk import Jarvis, MemoryHandle
|
|
|
|
|
|
def _make_engine(content="Hello from SDK"):
|
|
engine = MagicMock()
|
|
engine.engine_id = "mock"
|
|
engine.health.return_value = True
|
|
engine.list_models.return_value = ["test-model"]
|
|
engine.generate.return_value = {
|
|
"content": content,
|
|
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
|
|
"model": "test-model",
|
|
"finish_reason": "stop",
|
|
}
|
|
return engine
|
|
|
|
|
|
class TestJarvisInit:
|
|
def test_default_config(self):
|
|
j = Jarvis(config=JarvisConfig())
|
|
assert j.config is not None
|
|
j.close()
|
|
|
|
def test_custom_config(self):
|
|
cfg = JarvisConfig()
|
|
j = Jarvis(config=cfg)
|
|
assert j.config is cfg
|
|
j.close()
|
|
|
|
def test_version_property(self):
|
|
j = Jarvis(config=JarvisConfig())
|
|
assert j.version == openjarvis.__version__
|
|
j.close()
|
|
|
|
def test_engine_key_override(self):
|
|
j = Jarvis(config=JarvisConfig(), engine_key="custom")
|
|
assert j._engine_key == "custom"
|
|
j.close()
|
|
|
|
def test_model_override(self):
|
|
j = Jarvis(config=JarvisConfig(), model="my-model")
|
|
assert j._model_override == "my-model"
|
|
j.close()
|
|
|
|
|
|
class TestJarvisAsk:
|
|
def test_ask_returns_string(self):
|
|
engine = _make_engine("The answer is 42.")
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig(), model="test-model")
|
|
result = j.ask("What is the answer?")
|
|
assert result == "The answer is 42."
|
|
j.close()
|
|
|
|
def test_ask_with_model_override(self):
|
|
engine = _make_engine()
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig())
|
|
j.ask("Hello", model="custom-model")
|
|
# Verify engine.generate was called with the custom model
|
|
call_kwargs = engine.generate.call_args
|
|
assert call_kwargs[1]["model"] == "custom-model"
|
|
j.close()
|
|
|
|
def test_ask_with_agent(self):
|
|
from openjarvis.agents._stubs import AgentResult
|
|
from openjarvis.core.registry import AgentRegistry
|
|
|
|
engine = _make_engine()
|
|
|
|
class MockAgent:
|
|
agent_id = "mock-agent"
|
|
|
|
def __init__(self, eng, model, **kwargs):
|
|
pass
|
|
|
|
def run(self, input, context=None, **kwargs):
|
|
return AgentResult(content="Agent response", turns=1)
|
|
|
|
AgentRegistry.register_value("mock-agent", MockAgent)
|
|
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig(), model="test-model")
|
|
result = j.ask("Hello", agent="mock-agent")
|
|
assert result == "Agent response"
|
|
j.close()
|
|
|
|
def test_ask_no_engine_raises(self):
|
|
with patch("openjarvis.sdk.get_engine", return_value=None):
|
|
j = Jarvis(config=JarvisConfig())
|
|
with pytest.raises(RuntimeError, match="No inference engine"):
|
|
j.ask("Hello")
|
|
j.close()
|
|
|
|
def test_ask_full_returns_dict(self):
|
|
engine = _make_engine("Full response")
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig(), model="test-model")
|
|
result = j.ask_full("Hello")
|
|
assert isinstance(result, dict)
|
|
assert "content" in result
|
|
assert "usage" in result
|
|
assert result["content"] == "Full response"
|
|
j.close()
|
|
|
|
|
|
class TestJarvisModels:
|
|
def test_list_models(self):
|
|
engine = _make_engine()
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig())
|
|
models = j.list_models()
|
|
assert models == ["test-model"]
|
|
j.close()
|
|
|
|
def test_list_engines(self):
|
|
from openjarvis.core.registry import EngineRegistry
|
|
|
|
EngineRegistry.register_value("test-eng", object)
|
|
j = Jarvis(config=JarvisConfig())
|
|
engines = j.list_engines()
|
|
assert "test-eng" in engines
|
|
j.close()
|
|
|
|
def test_list_engines_empty(self):
|
|
j = Jarvis(config=JarvisConfig())
|
|
engines = j.list_engines()
|
|
assert isinstance(engines, list)
|
|
j.close()
|
|
|
|
|
|
class TestMemoryHandle:
|
|
def test_lazy_backend_init(self):
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
assert handle._backend is None
|
|
handle.close()
|
|
|
|
def test_close_idempotent(self):
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
handle.close()
|
|
handle.close() # should not raise
|
|
|
|
def test_index_file(self, tmp_path):
|
|
# Create a test file with enough content to produce chunks
|
|
test_file = tmp_path / "test.txt"
|
|
words = " ".join(f"word{i}" for i in range(100))
|
|
test_file.write_text(words)
|
|
|
|
# Mock the memory backend
|
|
mock_backend = MagicMock()
|
|
mock_backend.store.return_value = "doc-1"
|
|
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
handle._backend = mock_backend
|
|
|
|
result = handle.index(str(test_file))
|
|
assert result["chunks"] > 0
|
|
assert "doc_ids" in result
|
|
handle.close()
|
|
|
|
def test_search_returns_results(self):
|
|
mock_backend = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.content = "test content"
|
|
mock_result.score = 0.9
|
|
mock_result.source = "test.txt"
|
|
mock_result.metadata = {}
|
|
mock_backend.retrieve.return_value = [mock_result]
|
|
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
handle._backend = mock_backend
|
|
|
|
results = handle.search("test query")
|
|
assert len(results) == 1
|
|
assert results[0]["content"] == "test content"
|
|
handle.close()
|
|
|
|
def test_search_empty(self):
|
|
mock_backend = MagicMock()
|
|
mock_backend.retrieve.return_value = []
|
|
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
handle._backend = mock_backend
|
|
|
|
results = handle.search("nothing")
|
|
assert results == []
|
|
handle.close()
|
|
|
|
def test_stats_returns_dict(self):
|
|
mock_backend = MagicMock()
|
|
mock_backend.count.return_value = 5
|
|
|
|
cfg = JarvisConfig()
|
|
handle = MemoryHandle(cfg)
|
|
handle._backend = mock_backend
|
|
|
|
stats = handle.stats()
|
|
assert isinstance(stats, dict)
|
|
assert stats["count"] == 5
|
|
handle.close()
|
|
|
|
|
|
class TestJarvisStreaming:
|
|
@pytest.mark.asyncio
|
|
async def test_ask_stream_yields_tokens(self):
|
|
engine = _make_engine()
|
|
|
|
async def mock_stream(*args, **kwargs):
|
|
for token in ["Hello", " ", "world"]:
|
|
yield token
|
|
|
|
engine.stream = mock_stream
|
|
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig(), model="test-model")
|
|
tokens = []
|
|
async for token in j.ask_stream("Hi"):
|
|
tokens.append(token)
|
|
assert tokens == ["Hello", " ", "world"]
|
|
j.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ask_full_stream_yields_dicts(self):
|
|
engine = _make_engine()
|
|
|
|
async def mock_stream(*args, **kwargs):
|
|
for token in ["Hello", " ", "world"]:
|
|
yield token
|
|
|
|
engine.stream = mock_stream
|
|
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig(), model="test-model")
|
|
chunks = []
|
|
async for chunk in j.ask_full_stream("Hi"):
|
|
chunks.append(chunk)
|
|
|
|
# First three chunks are token dicts
|
|
assert chunks[0] == {"token": "Hello", "index": 0}
|
|
assert chunks[1] == {"token": " ", "index": 1}
|
|
assert chunks[2] == {"token": "world", "index": 2}
|
|
|
|
# Final chunk has done flag and full content
|
|
final = chunks[-1]
|
|
assert final["done"] is True
|
|
assert final["content"] == "Hello world"
|
|
assert final["model"] == "test-model"
|
|
assert final["engine"] == "mock"
|
|
j.close()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ask_stream_with_model_override(self):
|
|
engine = _make_engine()
|
|
call_log: list = []
|
|
|
|
async def mock_stream(*args, **kwargs):
|
|
call_log.append(kwargs)
|
|
for token in ["ok"]:
|
|
yield token
|
|
|
|
engine.stream = mock_stream
|
|
|
|
with patch("openjarvis.sdk.get_engine", return_value=("mock", engine)):
|
|
j = Jarvis(config=JarvisConfig())
|
|
tokens = []
|
|
async for token in j.ask_stream("Hi", model="custom-model"):
|
|
tokens.append(token)
|
|
assert tokens == ["ok"]
|
|
assert call_log[0]["model"] == "custom-model"
|
|
j.close()
|
|
|
|
|
|
class TestJarvisLifecycle:
|
|
def test_close_releases_resources(self):
|
|
j = Jarvis(config=JarvisConfig())
|
|
j.close()
|
|
assert j._engine is None
|
|
|
|
def test_double_close_safe(self):
|
|
j = Jarvis(config=JarvisConfig())
|
|
j.close()
|
|
j.close() # should not raise
|