Files
OpenJarvis/tests/sdk/test_sdk.py
T
Jon Saad-FalconandClaude Opus 4.6 70cbda8a73 chore: derive __version__ from metadata, bump GHA action versions
- 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>
2026-04-11 13:19:19 -07:00

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