fix(channels): wire channel→agent handler and fix Telegram send pipeline (#94)

* fix(channels): wire channel→agent handler and fix Telegram send pipeline

* format code

* add supported tests
This commit is contained in:
Prathap P
2026-03-20 18:35:18 -07:00
committed by GitHub
parent 62eaa70736
commit c2756964a7
195 changed files with 4080 additions and 2536 deletions
+1
View File
@@ -14,6 +14,7 @@ dependencies = [
"ddgs>=9.11.4",
"httpx>=0.27",
"openai>=1.30",
"python-telegram-bot>=22.6",
"rich>=13",
"tomli>=2.0; python_version < '3.11'",
]
+16 -4
View File
@@ -109,14 +109,13 @@ class TelegramChannel(BaseChannel):
import httpx
url = f"https://api.telegram.org/bot{self._token}/sendMessage"
chat_id = conversation_id or channel
payload: Dict[str, Any] = {
"chat_id": channel,
"chat_id": chat_id,
"text": content,
}
if self._parse_mode:
payload["parse_mode"] = self._parse_mode
if conversation_id:
payload["reply_to_message_id"] = conversation_id
resp = httpx.post(url, json=payload, timeout=10.0)
if resp.status_code < 300:
@@ -164,6 +163,19 @@ class TelegramChannel(BaseChannel):
message_id=str(msg.message_id),
conversation_id=str(msg.chat.id),
)
# Enforce allow-list when configured
if self._allowed_chat_ids:
_allowed = {
cid.strip()
for cid in self._allowed_chat_ids.split(",")
if cid.strip()
}
if cm.conversation_id not in _allowed:
logger.debug(
"Ignoring message from unlisted chat %s",
cm.conversation_id,
)
return
for handler in self._handlers:
try:
handler(cm)
@@ -181,7 +193,7 @@ class TelegramChannel(BaseChannel):
)
app.add_handler(MessageHandler(filters.TEXT, _handle_msg))
app.run_polling(stop_signals=None)
app.run_polling(stop_signals=None, drop_pending_updates=True)
except Exception:
logger.debug("Telegram poll loop error", exc_info=True)
self._status = ChannelStatus.ERROR
+14
View File
@@ -230,6 +230,20 @@ def serve(
console.print(f"[yellow]Channel failed to start: {exc}[/yellow]")
channel_bridge = None
# Wire channel messages → agent / engine (per-chat session isolation)
if channel_bridge is not None:
from openjarvis.system import JarvisSystem
_wire_system = JarvisSystem(
config=config,
bus=bus,
engine=engine,
engine_key=engine_name,
model=model_name,
agent_name=agent_key or "",
)
_wire_system.wire_channel(channel_bridge)
# Set up speech backend
speech_backend = None
try:
+1 -1
View File
@@ -66,7 +66,7 @@ class SessionStore:
) -> None:
self._db_path = Path(db_path)
self._db_path.parent.mkdir(parents=True, exist_ok=True)
self._conn = sqlite3.connect(str(self._db_path))
self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
self._max_age_hours = max_age_hours
self._consolidation_threshold = consolidation_threshold
self._create_tables()
+79
View File
@@ -269,6 +269,85 @@ class JarvisSystem:
logger.warning("Failed to build tool %r: %s", name, exc)
return tools
def wire_channel(self, channel_bridge: Any) -> None:
"""Register a message handler on *channel_bridge* that routes every
incoming message through this system (agent or engine) and replies.
Sessions are isolated per ``"<channel>:<conversation_id>"`` key so
each chat retains its own history.
Parameters
----------
channel_bridge:
A connected :class:`~openjarvis.channels._stubs.BaseChannel`
instance whose ``on_message`` method accepts a callable.
"""
from openjarvis.agents._stubs import AgentContext
from openjarvis.core.types import Message, Role
from openjarvis.sessions.session import SessionStore
if self.session_store is None:
from pathlib import Path
self.session_store = SessionStore(
db_path=Path(self.config.sessions.db_path).expanduser(),
max_age_hours=self.config.sessions.max_age_hours,
consolidation_threshold=self.config.sessions.consolidation_threshold,
)
_system = self # capture for closure
def _on_channel_message(cm) -> None:
session_key = f"{cm.channel}:{cm.conversation_id}"
session = _system.session_store.get_or_create(
session_key,
channel=cm.channel,
channel_user_id=cm.sender,
)
# Rebuild prior conversation turns into AgentContext
ctx = AgentContext()
for sm in session.messages:
try:
role = Role(sm.role)
except ValueError:
role = Role.USER
ctx.conversation.add(Message(role=role, content=sm.content))
reply = ""
try:
if _system.agent_name and _system.agent_name != "none":
result = _system.ask(
cm.content, context=False,
agent=_system.agent_name,
)
reply = result.get("content", "")
else:
result = _system.ask(cm.content, context=False)
reply = result.get("content", "")
except Exception:
logger.exception("Channel message handler error")
reply = "Sorry, I encountered an error processing your message."
try:
_system.session_store.save_message(
session.session_id, "user", cm.content, channel=cm.channel,
)
_system.session_store.save_message(
session.session_id, "assistant", reply, channel=cm.channel,
)
except Exception:
logger.debug("Session save error", exc_info=True)
if reply:
try:
channel_bridge.send(
cm.channel, reply, conversation_id=cm.conversation_id,
)
except Exception:
logger.exception("Channel send error")
channel_bridge.on_message(_on_channel_message)
def close(self) -> None:
"""Release resources."""
if self.scheduler and hasattr(self.scheduler, "stop"):
+81 -60
View File
@@ -78,6 +78,7 @@ class TestA2AResponse:
def test_from_json(self):
import json
data = json.dumps({"jsonrpc": "2.0", "result": "ok", "id": "3"})
resp = A2AResponse.from_json(data)
assert resp.result == "ok"
@@ -88,24 +89,28 @@ class TestA2AServer:
def test_task_send(self):
card = AgentCard(name="Test")
server = A2AServer(card, handler=lambda x: f"Echo: {x}")
response = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "Hello"},
"id": "1",
})
response = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "Hello"},
"id": "1",
}
)
assert response["result"]["state"] == "completed"
assert "Echo: Hello" in response["result"]["output"]
def test_task_send_with_message_format(self):
card = AgentCard(name="Test")
server = A2AServer(card, handler=lambda x: f"Got: {x}")
response = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"message": {"role": "user", "parts": [{"text": "Hi"}]}},
"id": "1",
})
response = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"message": {"role": "user", "parts": [{"text": "Hi"}]}},
"id": "1",
}
)
assert response["result"]["state"] == "completed"
assert "Got: Hi" in response["result"]["output"]
@@ -113,62 +118,74 @@ class TestA2AServer:
card = AgentCard(name="Test")
server = A2AServer(card, handler=lambda x: x)
# First send a task
send_resp = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
})
send_resp = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
}
)
task_id = send_resp["result"]["id"]
# Now get it
get_resp = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"id": task_id},
"id": "2",
})
get_resp = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"id": task_id},
"id": "2",
}
)
assert get_resp["result"]["id"] == task_id
def test_task_get_not_found(self):
card = AgentCard(name="Test")
server = A2AServer(card)
response = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"id": "nonexistent"},
"id": "1",
})
response = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"id": "nonexistent"},
"id": "1",
}
)
assert "error" in response
def test_task_cancel(self):
card = AgentCard(name="Test")
server = A2AServer(card, handler=lambda x: x)
send_resp = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
})
send_resp = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
}
)
task_id = send_resp["result"]["id"]
cancel_resp = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/cancel",
"params": {"id": task_id},
"id": "2",
})
cancel_resp = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/cancel",
"params": {"id": task_id},
"id": "2",
}
)
assert cancel_resp["result"]["state"] == "canceled"
def test_unknown_method(self):
card = AgentCard(name="Test")
server = A2AServer(card)
response = server.handle_request({
"jsonrpc": "2.0",
"method": "unknown/method",
"params": {},
"id": "1",
})
response = server.handle_request(
{
"jsonrpc": "2.0",
"method": "unknown/method",
"params": {},
"id": "1",
}
)
assert "error" in response
assert response["error"]["code"] == -32601
@@ -176,12 +193,14 @@ class TestA2AServer:
bus = EventBus(record_history=True)
card = AgentCard(name="Test")
server = A2AServer(card, handler=lambda x: x, bus=bus)
server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
})
server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
}
)
event_types = {e.event_type for e in bus.history}
assert EventType.A2A_TASK_RECEIVED in event_types
assert EventType.A2A_TASK_COMPLETED in event_types
@@ -193,10 +212,12 @@ class TestA2AServer:
raise ValueError("boom")
server = A2AServer(card, handler=bad_handler)
response = server.handle_request({
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
})
response = server.handle_request(
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {"input": "test"},
"id": "1",
}
)
assert response["result"]["state"] == "failed"
+8 -2
View File
@@ -87,7 +87,11 @@ class TestBaseAgentInit:
bus = EventBus()
engine = MagicMock()
agent = _ConcreteAgent(
engine, "m", bus=bus, temperature=0.1, max_tokens=256,
engine,
"m",
bus=bus,
temperature=0.1,
max_tokens=256,
)
assert agent._temperature == 0.1
assert agent._max_tokens == 256
@@ -176,7 +180,9 @@ class TestBuildMessages:
conv.add(Message(role=Role.USER, content="prev"))
ctx = AgentContext(conversation=conv)
messages = agent._build_messages(
"new", ctx, system_prompt="System.",
"new",
ctx,
system_prompt="System.",
)
assert len(messages) == 3
assert messages[0].role == Role.SYSTEM
+3 -3
View File
@@ -20,8 +20,7 @@ def test_budget_exceeded_sets_status(tmp_path):
assert updated["status"] == "budget_exceeded"
budget_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_BUDGET_EXCEEDED
e for e in bus.history if e.event_type == EventType.AGENT_BUDGET_EXCEEDED
]
assert len(budget_events) == 1
mgr.close()
@@ -54,7 +53,8 @@ def test_budget_unlimited_skips_check(tmp_path):
mgr.start_tick(agent["id"])
result = AgentResult(
content="done", metadata={"cost": 999.99, "tokens_used": 1000000},
content="done",
metadata={"cost": 999.99, "tokens_used": 1000000},
)
executor._finalize_tick(agent["id"], result, error=None, duration=1.0)
+99 -63
View File
@@ -141,16 +141,19 @@ class TestClaudeCodeRun:
def test_successful_run(self):
agent = self._make_agent()
output = _wrap_output({
"content": "Hello from Claude Code!",
"tool_results": [],
"metadata": {"message_count": 3},
})
output = _wrap_output(
{
"content": "Hello from Claude Code!",
"tool_results": [],
"metadata": {"message_count": 3},
}
)
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -165,22 +168,25 @@ class TestClaudeCodeRun:
def test_run_with_tool_results(self):
agent = self._make_agent()
output = _wrap_output({
"content": "I read the file.",
"tool_results": [
{
"tool_name": "Read",
"content": "file contents",
"success": True,
},
],
"metadata": {},
})
output = _wrap_output(
{
"content": "I read the file.",
"tool_results": [
{
"tool_name": "Read",
"content": "file contents",
"success": True,
},
],
"metadata": {},
}
)
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -200,20 +206,24 @@ class TestClaudeCodeRun:
allowed_tools=["Read", "Write"],
system_prompt="Be helpful.",
)
output = _wrap_output({
"content": "ok",
"tool_results": [],
"metadata": {},
})
output = _wrap_output(
{
"content": "ok",
"tool_results": [],
"metadata": {},
}
)
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch(
"subprocess.run", return_value=proc,
"subprocess.run",
return_value=proc,
) as mock_run,
):
agent.run("Do something")
@@ -230,12 +240,14 @@ class TestClaudeCodeRun:
def test_timeout_handling(self):
agent = self._make_agent(timeout=5)
exc = subprocess.TimeoutExpired(
cmd="node", timeout=5,
cmd="node",
timeout=5,
)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", side_effect=exc),
@@ -249,12 +261,14 @@ class TestClaudeCodeRun:
def test_nonzero_exit_code(self):
agent = self._make_agent()
proc = _mock_proc(
returncode=1, stderr="ENOENT: module not found",
returncode=1,
stderr="ENOENT: module not found",
)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -273,7 +287,8 @@ class TestClaudeCodeRun:
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -291,7 +306,8 @@ class TestClaudeCodeRun:
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -312,18 +328,24 @@ class TestClaudeCodeEvents:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", bus=bus, api_key="k",
engine,
"test-model",
bus=bus,
api_key="k",
)
output = _wrap_output(
{
"content": "hi",
"tool_results": [],
"metadata": {},
}
)
output = _wrap_output({
"content": "hi",
"tool_results": [],
"metadata": {},
})
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -339,18 +361,24 @@ class TestClaudeCodeEvents:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", bus=bus, api_key="k",
engine,
"test-model",
bus=bus,
api_key="k",
)
output = _wrap_output(
{
"content": "hi",
"tool_results": [],
"metadata": {},
}
)
output = _wrap_output({
"content": "hi",
"tool_results": [],
"metadata": {},
})
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -358,8 +386,7 @@ class TestClaudeCodeEvents:
agent.run("test input")
start_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_TURN_START
e for e in bus.history if e.event_type == EventType.AGENT_TURN_START
]
assert len(start_events) == 1
assert start_events[0].data["agent"] == "claude_code"
@@ -370,13 +397,17 @@ class TestClaudeCodeEvents:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", bus=bus, api_key="k",
engine,
"test-model",
bus=bus,
api_key="k",
)
proc = _mock_proc(returncode=1, stderr="error")
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
@@ -449,11 +480,7 @@ class TestParseOutput:
"tool_results": [],
"metadata": {},
}
stdout = (
"some debug output\n"
+ _wrap_output(payload)
+ "\nmore output"
)
stdout = "some debug output\n" + _wrap_output(payload) + "\nmore output"
content, tools, meta = ClaudeCodeAgent._parse_output(
stdout,
)
@@ -485,7 +512,9 @@ class TestClaudeCodeDefaults:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", api_key="explicit-key",
engine,
"test-model",
api_key="explicit-key",
)
assert agent._api_key == "explicit-key"
@@ -499,7 +528,9 @@ class TestClaudeCodeDefaults:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", timeout=60,
engine,
"test-model",
timeout=60,
)
assert agent._timeout == 60
@@ -507,18 +538,23 @@ class TestClaudeCodeDefaults:
engine = MagicMock()
engine.engine_id = "mock"
agent = ClaudeCodeAgent(
engine, "test-model", api_key="k",
engine,
"test-model",
api_key="k",
)
output = _wrap_output(
{
"content": "ok",
"tool_results": [],
"metadata": {},
}
)
output = _wrap_output({
"content": "ok",
"tool_results": [],
"metadata": {},
})
proc = _mock_proc(stdout=output)
with (
patch.object(
agent, "_ensure_runner",
agent,
"_ensure_runner",
return_value="/fake/runner",
),
patch("subprocess.run", return_value=proc),
+31 -21
View File
@@ -43,39 +43,47 @@ class ContinuationAgent(BaseAgent):
class TestContinuation:
def test_no_continuation_needed(self):
engine = MockEngine([
{"content": "Hello world", "finish_reason": "stop"},
])
engine = MockEngine(
[
{"content": "Hello world", "finish_reason": "stop"},
]
)
agent = ContinuationAgent(engine, "test-model")
result = agent.run("Hi")
assert result.content == "Hello world"
def test_single_continuation(self):
engine = MockEngine([
{"content": "Part 1...", "finish_reason": "length"},
{"content": " Part 2.", "finish_reason": "stop"},
])
engine = MockEngine(
[
{"content": "Part 1...", "finish_reason": "length"},
{"content": " Part 2.", "finish_reason": "stop"},
]
)
agent = ContinuationAgent(engine, "test-model")
result = agent.run("Hi")
assert result.content == "Part 1... Part 2."
def test_multiple_continuations(self):
engine = MockEngine([
{"content": "A", "finish_reason": "length"},
{"content": "B", "finish_reason": "length"},
{"content": "C", "finish_reason": "stop"},
])
engine = MockEngine(
[
{"content": "A", "finish_reason": "length"},
{"content": "B", "finish_reason": "length"},
{"content": "C", "finish_reason": "stop"},
]
)
agent = ContinuationAgent(engine, "test-model")
result = agent.run("Hi")
assert result.content == "ABC"
def test_max_continuations_respected(self):
engine = MockEngine([
{"content": "A", "finish_reason": "length"},
{"content": "B", "finish_reason": "length"},
{"content": "C", "finish_reason": "length"}, # 3rd continuation
{"content": "D", "finish_reason": "stop"},
])
engine = MockEngine(
[
{"content": "A", "finish_reason": "length"},
{"content": "B", "finish_reason": "length"},
{"content": "C", "finish_reason": "length"}, # 3rd continuation
{"content": "D", "finish_reason": "stop"},
]
)
agent = ContinuationAgent(engine, "test-model")
# Default max_continuations=2, so should stop after 2 continuations
messages = agent._build_messages("Hi")
@@ -84,9 +92,11 @@ class TestContinuation:
assert content == "ABC" # A + B + C, but not D
def test_empty_finish_reason(self):
engine = MockEngine([
{"content": "Done", "finish_reason": ""},
])
engine = MockEngine(
[
{"content": "Done", "finish_reason": ""},
]
)
agent = ContinuationAgent(engine, "test-model")
result = agent.run("Hi")
assert result.content == "Done"
@@ -1,4 +1,5 @@
"""Tests for structured error_detail in executor traces."""
from openjarvis.agents.errors import EscalateError, FatalError, RetryableError
from openjarvis.agents.executor import AgentExecutor
from openjarvis.core.events import EventBus
@@ -6,6 +7,7 @@ from openjarvis.core.events import EventBus
def test_build_error_detail_fatal(tmp_path):
from openjarvis.agents.manager import AgentManager
mgr = AgentManager(db_path=str(tmp_path / "agents.db"))
exe = AgentExecutor(manager=mgr, event_bus=EventBus())
error = FatalError("401 unauthorized")
@@ -17,6 +19,7 @@ def test_build_error_detail_fatal(tmp_path):
def test_build_error_detail_retryable(tmp_path):
from openjarvis.agents.manager import AgentManager
mgr = AgentManager(db_path=str(tmp_path / "agents.db"))
exe = AgentExecutor(manager=mgr, event_bus=EventBus())
error = RetryableError("connection timed out")
@@ -27,6 +30,7 @@ def test_build_error_detail_retryable(tmp_path):
def test_build_error_detail_escalate(tmp_path):
from openjarvis.agents.manager import AgentManager
mgr = AgentManager(db_path=str(tmp_path / "agents.db"))
exe = AgentExecutor(manager=mgr, event_bus=EventBus())
error = EscalateError("agent needs help")
+17 -13
View File
@@ -50,11 +50,14 @@ def test_scheduler_tracks_tick_count_for_learning(tmp_path):
executor = AgentExecutor(mgr, bus)
scheduler = AgentScheduler(mgr, executor, event_bus=bus)
agent = mgr.create_agent("tick-counter", config={
"learning_enabled": True,
"learning_schedule": "every_3_ticks",
"schedule_type": "manual",
})
agent = mgr.create_agent(
"tick-counter",
config={
"learning_enabled": True,
"learning_schedule": "every_3_ticks",
"schedule_type": "manual",
},
)
# Simulate 3 ticks completing
for _ in range(3):
@@ -62,8 +65,7 @@ def test_scheduler_tracks_tick_count_for_learning(tmp_path):
# Should have triggered learning
learning_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_LEARNING_STARTED
e for e in bus.history if e.event_type == EventType.AGENT_LEARNING_STARTED
]
assert len(learning_events) == 1
assert learning_events[0].data["agent_id"] == agent["id"]
@@ -85,17 +87,19 @@ def test_scheduler_no_learning_when_disabled(tmp_path):
executor = AgentExecutor(mgr, bus)
scheduler = AgentScheduler(mgr, executor, event_bus=bus)
agent = mgr.create_agent("no-learning", config={
"learning_enabled": False,
"learning_schedule": "every_3_ticks",
})
agent = mgr.create_agent(
"no-learning",
config={
"learning_enabled": False,
"learning_schedule": "every_3_ticks",
},
)
for _ in range(5):
scheduler._on_tick_completed(agent["id"])
learning_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_LEARNING_STARTED
e for e in bus.history if e.event_type == EventType.AGENT_LEARNING_STARTED
]
assert len(learning_events) == 0
mgr.close()
+31 -28
View File
@@ -8,6 +8,7 @@ from openjarvis.core.events import EventBus, EventType
class TestLoopGuard:
def _make_guard(self, **kwargs):
from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig
kwargs.setdefault("warn_before_block", False)
config = LoopGuardConfig(**kwargs)
bus = EventBus(record_history=True)
@@ -55,8 +56,7 @@ class TestLoopGuard:
guard.check_call("x", '{"a": 1}')
guard.check_call("x", '{"a": 1}')
events = [
e for e in bus.history
if e.event_type == EventType.LOOP_GUARD_TRIGGERED
e for e in bus.history if e.event_type == EventType.LOOP_GUARD_TRIGGERED
]
assert len(events) == 1
@@ -70,6 +70,7 @@ class TestLoopGuard:
def test_context_compression_no_overflow(self):
from openjarvis.core.types import Message, Role
guard, _ = self._make_guard(max_context_messages=100)
messages = [Message(role=Role.USER, content=f"msg {i}") for i in range(10)]
result = guard.compress_context(messages)
@@ -77,42 +78,43 @@ class TestLoopGuard:
def test_context_compression_with_overflow(self):
from openjarvis.core.types import Message, Role
guard, _ = self._make_guard(max_context_messages=10)
messages = [
Message(role=Role.SYSTEM, content="sys"),
] + [
Message(role=Role.USER, content=f"msg {i}")
for i in range(50)
] + [
Message(role=Role.TOOL, content=f"result {i}", tool_call_id=f"t{i}")
for i in range(50)
]
messages = (
[
Message(role=Role.SYSTEM, content="sys"),
]
+ [Message(role=Role.USER, content=f"msg {i}") for i in range(50)]
+ [
Message(role=Role.TOOL, content=f"result {i}", tool_call_id=f"t{i}")
for i in range(50)
]
)
result = guard.compress_context(messages)
assert len(result) <= 10
def test_context_compression_stage4_uses_current_state(self):
"""Stage 4 should derive from compressed state."""
from openjarvis.core.types import Message, Role
guard, _ = self._make_guard(max_context_messages=5)
messages = [
Message(role=Role.SYSTEM, content="sys"),
] + [
Message(role=Role.USER, content=f"msg {i}")
for i in range(100)
] + [
Message(
role=Role.TOOL,
content=f"result {i}",
tool_call_id=f"t{i}",
)
for i in range(100)
]
messages = (
[
Message(role=Role.SYSTEM, content="sys"),
]
+ [Message(role=Role.USER, content=f"msg {i}") for i in range(100)]
+ [
Message(
role=Role.TOOL,
content=f"result {i}",
tool_call_id=f"t{i}",
)
for i in range(100)
]
)
result = guard.compress_context(messages)
assert len(result) == 5
system_count = sum(
1 for m in result
if getattr(m, 'role', None) == 'system'
)
system_count = sum(1 for m in result if getattr(m, "role", None) == "system")
assert system_count == 1
def test_check_response_returns_unblocked(self):
@@ -122,6 +124,7 @@ class TestLoopGuard:
def test_disabled_loop_guard(self):
from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig
config = LoopGuardConfig(enabled=False)
guard = LoopGuard(config)
# Even though we'd normally block, disabled guard shouldn't
+9 -3
View File
@@ -7,7 +7,9 @@ from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig, LoopVerdict
def test_warn_before_block_first_cycle_warns():
config = LoopGuardConfig(
enabled=True, max_identical_calls=2, warn_before_block=True,
enabled=True,
max_identical_calls=2,
warn_before_block=True,
)
guard = LoopGuard(config)
# Simulate the Rust backend blocking on the second identical call
@@ -25,7 +27,9 @@ def test_warn_before_block_first_cycle_warns():
def test_warn_before_block_second_cycle_blocks():
config = LoopGuardConfig(
enabled=True, max_identical_calls=2, warn_before_block=True,
enabled=True,
max_identical_calls=2,
warn_before_block=True,
)
guard = LoopGuard(config)
mock_rust = MagicMock()
@@ -47,7 +51,9 @@ def test_warn_before_block_second_cycle_blocks():
def test_default_behavior_unchanged():
config = LoopGuardConfig(
enabled=True, max_identical_calls=2, warn_before_block=False,
enabled=True,
max_identical_calls=2,
warn_before_block=False,
)
guard = LoopGuard(config)
mock_rust = MagicMock()
+1
View File
@@ -1,4 +1,5 @@
"""Tests for AgentManager.recover_agent() always resetting status."""
import pytest
from openjarvis.agents.manager import AgentManager
+3 -1
View File
@@ -23,6 +23,7 @@ class TestMonitorOperativeAgent:
# Import triggers registration; re-register after autouse fixture
# clears the registry (same pattern as test_monitor.py)
import openjarvis.agents.monitor_operative # noqa: F401
if not AgentRegistry.contains("monitor_operative"):
AgentRegistry.register_value("monitor_operative", MonitorOperativeAgent)
assert AgentRegistry.contains("monitor_operative")
@@ -46,7 +47,8 @@ class TestMonitorOperativeAgent:
def test_custom_strategies(self) -> None:
engine = _make_engine()
agent = MonitorOperativeAgent(
engine, "test-model",
engine,
"test-model",
memory_extraction="scratchpad",
observation_compression="none",
retrieval_strategy="keyword",
+28 -25
View File
@@ -38,6 +38,7 @@ class _CodeInterpreterStub(BaseTool):
# Simple simulation: if it contains print(), capture the content
if "print(" in code:
import re
match = re.search(r"print\((.+?)\)", code)
if match:
try:
@@ -134,13 +135,12 @@ class TestNativeOpenHandsAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
"Let me calculate:\n```python\nprint(2+2)\n```"
),
_engine_response("Let me calculate:\n```python\nprint(2+2)\n```"),
_engine_response("The result is 4."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
)
result = agent.run("What is 2+2?")
@@ -159,7 +159,8 @@ class TestNativeOpenHandsAgent:
_engine_response("First was 2, second was 9."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
)
result = agent.run("Two calculations")
@@ -175,7 +176,8 @@ class TestNativeOpenHandsAgent:
"More code:\n```python\nprint('hello')\n```"
)
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
max_turns=3,
)
@@ -205,7 +207,8 @@ class TestNativeOpenHandsAgent:
_engine_response("Done."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
bus=bus,
)
@@ -238,13 +241,12 @@ class TestNativeOpenHandsAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Action: calculator\nAction Input: {"expression": "7*6"}'
),
_engine_response('Action: calculator\nAction Input: {"expression": "7*6"}'),
_engine_response("The answer is 42."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 7 times 6?")
@@ -285,7 +287,8 @@ class TestNativeOpenHandsAgent:
engine.engine_id = "mock"
engine.generate.return_value = _engine_response("Ok")
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub(), _CalculatorStub()],
)
agent.run("Hello")
@@ -301,7 +304,8 @@ class TestNativeOpenHandsAgent:
engine.engine_id = "mock"
engine.generate.return_value = _engine_response("Ok")
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub(), _CalculatorStub()],
)
agent.run("Hello")
@@ -321,7 +325,8 @@ class TestNativeOpenHandsAgent:
"Still working:\n```python\nx = 1\n```"
)
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
max_turns=2,
)
@@ -339,7 +344,8 @@ class TestNativeOpenHandsAgent:
_engine_response("Got 42."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CodeInterpreterStub()],
)
agent.run("Print 42")
@@ -358,8 +364,7 @@ class TestNativeOpenHandsAgent:
agent = NativeOpenHandsAgent(engine, "test-model", bus=bus)
agent.run("test input")
start_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_TURN_START
e for e in bus.history if e.event_type == EventType.AGENT_TURN_START
]
assert len(start_events) == 1
assert start_events[0].data["agent"] == "native_openhands"
@@ -389,13 +394,12 @@ class TestNativeOpenHandsAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'<tool_call>calculator\n$expression=7*6</calculator>'
),
_engine_response("<tool_call>calculator\n$expression=7*6</calculator>"),
_engine_response("The answer is 42."),
]
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 7 times 6?")
@@ -408,9 +412,7 @@ class TestNativeOpenHandsAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Action: calculator\nAction Input: {"expression": "1+1"}'
),
_engine_response('Action: calculator\nAction Input: {"expression": "1+1"}'),
_engine_response("Done."),
]
# Make calculator return very long output
@@ -512,7 +514,8 @@ class TestUrlExpansion:
import httpx
monkeypatch.setattr(
httpx, "get",
httpx,
"get",
MagicMock(side_effect=Exception("Connection error")),
)
text, expanded = NativeOpenHandsAgent._expand_urls(
+42 -41
View File
@@ -111,8 +111,8 @@ class TestNativeReActParsing:
def test_parse_thought_action(self):
parse = self._parser()
text = (
'Thought: I need to calculate 2+2.\n'
'Action: calculator\n'
"Thought: I need to calculate 2+2.\n"
"Action: calculator\n"
'Action Input: {"expression": "2+2"}'
)
result = parse(text)
@@ -157,8 +157,8 @@ class TestNativeReActParsing:
def test_parse_case_insensitive_thought_action(self):
parse = self._parser()
text = (
'thought: I need to calculate 2+2.\n'
'action: calculator\n'
"thought: I need to calculate 2+2.\n"
"action: calculator\n"
'action input: {"expression": "2+2"}'
)
result = parse(text)
@@ -199,18 +199,18 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: I need to calculate.\n'
'Action: calculator\n'
"Thought: I need to calculate.\n"
"Action: calculator\n"
'Action Input: {"expression": "2+2"}'
),
_engine_response(
"Thought: The result is 4.\nFinal Answer: 4"
),
_engine_response("Thought: The result is 4.\nFinal Answer: 4"),
]
bus = EventBus(record_history=True)
agent = NativeReActAgent(
engine, "test-model",
tools=[_CalculatorStub()], bus=bus,
engine,
"test-model",
tools=[_CalculatorStub()],
bus=bus,
)
result = agent.run("What is 2+2?")
assert result.content == "4"
@@ -225,7 +225,7 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Calculate.\nAction: calculator\n'
"Thought: Calculate.\nAction: calculator\n"
'Action Input: {"expression": "3*7"}'
),
_engine_response("Thought: Done.\nFinal Answer: 21"),
@@ -241,21 +241,22 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Step 1.\nAction: calculator\n'
"Thought: Step 1.\nAction: calculator\n"
'Action Input: {"expression": "1+1"}'
),
_engine_response(
'Thought: Step 2.\nAction: calculator\n'
"Thought: Step 2.\nAction: calculator\n"
'Action Input: {"expression": "2+2"}'
),
_engine_response(
'Thought: Step 3.\nAction: think\n'
"Thought: Step 3.\nAction: think\n"
'Action Input: {"thought": "combining results"}'
),
_engine_response("Thought: All done.\nFinal Answer: Complete."),
]
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub(), _ThinkStub()],
)
result = agent.run("Multi step")
@@ -268,11 +269,12 @@ class TestNativeReActAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = _engine_response(
'Thought: Keep going.\nAction: calculator\n'
"Thought: Keep going.\nAction: calculator\n"
'Action Input: {"expression": "1+1"}'
)
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
max_turns=3,
)
@@ -287,12 +289,10 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Use a tool.\nAction: nonexistent\n'
'Action Input: {}'
"Thought: Use a tool.\nAction: nonexistent\nAction Input: {}"
),
_engine_response(
"Thought: Error occurred.\n"
"Final Answer: Could not run tool."
"Thought: Error occurred.\nFinal Answer: Could not run tool."
),
]
agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()])
@@ -322,14 +322,16 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Calc.\nAction: calculator\n'
"Thought: Calc.\nAction: calculator\n"
'Action Input: {"expression": "1+1"}'
),
_engine_response("Thought: Done.\nFinal Answer: 2"),
]
agent = NativeReActAgent(
engine, "test-model",
tools=[_CalculatorStub()], bus=bus,
engine,
"test-model",
tools=[_CalculatorStub()],
bus=bus,
)
agent.run("Calc")
event_types = [e.event_type for e in bus.history]
@@ -365,7 +367,7 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Let me reason.\nAction: think\n'
"Thought: Let me reason.\nAction: think\n"
'Action Input: {"thought": "The user wants a greeting"}'
),
_engine_response("Thought: Now I know.\nFinal Answer: Greetings!"),
@@ -403,7 +405,7 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'Thought: Calc.\nAction: calculator\n'
"Thought: Calc.\nAction: calculator\n"
'Action Input: {"expression": "5+5"}'
),
_engine_response("Thought: Got it.\nFinal Answer: 10"),
@@ -427,7 +429,8 @@ class TestNativeReActAgent:
"Thought: Done.\nFinal Answer: ok"
)
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub(), _ThinkStub()],
)
agent.run("Hello")
@@ -455,11 +458,11 @@ class TestNativeReActAgent:
engine = MagicMock()
engine.engine_id = "mock"
engine.generate.return_value = _engine_response(
'Thought: Go.\nAction: calculator\n'
'Action Input: {"expression": "1"}'
'Thought: Go.\nAction: calculator\nAction Input: {"expression": "1"}'
)
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
max_turns=1,
)
@@ -478,14 +481,12 @@ class TestNativeReActAgent:
agent = NativeReActAgent(engine, "test-model", bus=bus)
agent.run("test input")
start_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_TURN_START
e for e in bus.history if e.event_type == EventType.AGENT_TURN_START
]
assert len(start_events) == 1
assert start_events[0].data["agent"] == "native_react"
assert start_events[0].data["input"] == "test input"
def test_system_prompt_enriched_descriptions(self):
"""System prompt should include parameter schemas, not just names."""
engine = MagicMock()
@@ -494,7 +495,8 @@ class TestNativeReActAgent:
"Thought: Done.\nFinal Answer: ok"
)
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub(), _ThinkStub()],
)
agent.run("Hello")
@@ -514,16 +516,15 @@ class TestNativeReActAgent:
engine.engine_id = "mock"
engine.generate.side_effect = [
_engine_response(
'thought: I need to calculate.\n'
'action: calculator\n'
"thought: I need to calculate.\n"
"action: calculator\n"
'action input: {"expression": "2+2"}'
),
_engine_response(
"thought: The result is 4.\nfinal answer: 4"
),
_engine_response("thought: The result is 4.\nfinal answer: 4"),
]
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 2+2?")
+72 -34
View File
@@ -114,11 +114,13 @@ def _make_engine_multi_tool() -> MagicMock:
"content": "",
"tool_calls": [
{
"id": "call_1", "name": "calculator",
"id": "call_1",
"name": "calculator",
"arguments": '{"expression":"2+2"}',
},
{
"id": "call_2", "name": "think",
"id": "call_2",
"name": "think",
"arguments": '{"thought":"thinking..."}',
},
],
@@ -158,7 +160,9 @@ class TestOrchestratorAgent:
def test_single_tool_call(self):
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 2+2?")
assert result.content == "The answer is 4."
@@ -170,7 +174,8 @@ class TestOrchestratorAgent:
def test_multiple_tool_calls_same_turn(self):
engine = _make_engine_multi_tool()
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub(), _ThinkStub()],
)
result = agent.run("Think and calculate.")
@@ -193,7 +198,9 @@ class TestOrchestratorAgent:
def test_tools_passed_to_engine(self):
engine = _make_engine_no_tools()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
agent.run("Hello")
call_kwargs = engine.generate.call_args[1]
@@ -221,7 +228,8 @@ class TestOrchestratorAgent:
"finish_reason": "tool_calls",
}
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
max_turns=3,
)
@@ -236,7 +244,9 @@ class TestOrchestratorAgent:
final_content="Handled.",
)
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("Use unknown tool")
assert result.content == "Handled."
@@ -281,7 +291,10 @@ class TestOrchestratorAgent:
bus = EventBus(record_history=True)
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()], bus=bus,
engine,
"test-model",
tools=[_CalculatorStub()],
bus=bus,
)
agent.run("Calc 2+2")
event_types = [e.event_type for e in bus.history]
@@ -292,7 +305,9 @@ class TestOrchestratorAgent:
"""After tool call, messages include assistant + tool messages."""
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
agent.run("What is 2+2?")
# Second call should include accumulated messages
@@ -305,7 +320,9 @@ class TestOrchestratorAgent:
def test_tool_message_has_tool_call_id(self):
engine = _make_engine_with_tool_call(tool_call_id="abc123")
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
agent.run("What is 2+2?")
second_call = engine.generate.call_args_list[1]
@@ -317,7 +334,9 @@ class TestOrchestratorAgent:
def test_no_bus_works(self):
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 2+2?")
assert result.content == "The answer is 4."
@@ -335,10 +354,13 @@ class TestOrchestratorAgent:
engine.generate.side_effect = [
{
"content": "",
"tool_calls": [{
"id": "c1", "name": "calculator",
"arguments": '{"expression":"2+2"}',
}],
"tool_calls": [
{
"id": "c1",
"name": "calculator",
"arguments": '{"expression":"2+2"}',
}
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 3,
@@ -349,10 +371,13 @@ class TestOrchestratorAgent:
},
{
"content": "",
"tool_calls": [{
"id": "c2", "name": "calculator",
"arguments": '{"expression":"4*3"}',
}],
"tool_calls": [
{
"id": "c2",
"name": "calculator",
"arguments": '{"expression":"4*3"}',
}
],
"usage": {
"prompt_tokens": 15,
"completion_tokens": 3,
@@ -373,7 +398,9 @@ class TestOrchestratorAgent:
},
]
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("Calculate")
assert result.turns == 3
@@ -384,7 +411,9 @@ class TestOrchestratorAgent:
def test_tool_result_latency_tracked(self):
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
)
result = agent.run("What is 2+2?")
assert result.tool_results[0].latency_seconds >= 0
@@ -403,7 +432,8 @@ class TestOrchestratorAgent:
"finish_reason": "tool_calls",
}
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
max_turns=1,
)
@@ -433,7 +463,8 @@ class TestOrchestratorAgent:
"finish_reason": "tool_calls",
}
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
max_turns=2,
)
@@ -456,7 +487,9 @@ class TestOrchestratorStructuredMode:
"finish_reason": "stop",
}
agent = OrchestratorAgent(
engine, "test-model", mode="structured",
engine,
"test-model",
mode="structured",
)
result = agent.run("What is the capital of France?")
assert result.content == "Paris"
@@ -471,7 +504,7 @@ class TestOrchestratorStructuredMode:
{
"content": (
"THOUGHT: Need to calculate.\n"
'TOOL: calculator\n'
"TOOL: calculator\n"
'INPUT: {"expression":"2+2"}'
),
"usage": {
@@ -483,10 +516,7 @@ class TestOrchestratorStructuredMode:
"finish_reason": "stop",
},
{
"content": (
"THOUGHT: Got 4.\n"
"FINAL_ANSWER: The answer is 4."
),
"content": ("THOUGHT: Got 4.\nFINAL_ANSWER: The answer is 4."),
"usage": {
"prompt_tokens": 20,
"completion_tokens": 10,
@@ -497,7 +527,8 @@ class TestOrchestratorStructuredMode:
},
]
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
mode="structured",
)
@@ -519,7 +550,8 @@ class TestOrchestratorStructuredMode:
"finish_reason": "stop",
}
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub()],
mode="structured",
)
@@ -591,7 +623,10 @@ class TestOrchestratorParallelTools:
]
agent = OrchestratorAgent(
engine, "test-model", tools=[_SlowTool()], parallel_tools=True,
engine,
"test-model",
tools=[_SlowTool()],
parallel_tools=True,
)
t0 = time.time()
result = agent.run("Do things")
@@ -610,7 +645,8 @@ class TestOrchestratorParallelTools:
"""parallel_tools=False runs tools sequentially."""
engine = _make_engine_multi_tool()
agent = OrchestratorAgent(
engine, "test-model",
engine,
"test-model",
tools=[_CalculatorStub(), _ThinkStub()],
parallel_tools=False,
)
@@ -622,7 +658,9 @@ class TestOrchestratorParallelTools:
"""Single tool call should not use parallel path even if parallel_tools=True."""
engine = _make_engine_with_tool_call()
agent = OrchestratorAgent(
engine, "test-model", tools=[_CalculatorStub()],
engine,
"test-model",
tools=[_CalculatorStub()],
parallel_tools=True,
)
result = agent.run("What is 2+2?")
+4 -13
View File
@@ -197,9 +197,7 @@ class TestRLMSubLMCalls:
engine.generate.side_effect = [
{
"content": (
"```python\n"
"result = llm_query('What is 2+2?')\n"
"FINAL(result)\n```"
"```python\nresult = llm_query('What is 2+2?')\nFINAL(result)\n```"
),
"usage": {
"prompt_tokens": 5,
@@ -236,10 +234,7 @@ class TestRLMMultiTurn:
engine.generate.side_effect = [
# Turn 1: code that sets a variable
{
"content": (
"```python\n"
"x = 10\nprint(f'x = {x}')\n```"
),
"content": ("```python\nx = 10\nprint(f'x = {x}')\n```"),
"usage": {
"prompt_tokens": 5,
"completion_tokens": 10,
@@ -250,9 +245,7 @@ class TestRLMMultiTurn:
},
# Turn 2: code that uses the variable and terminates
{
"content": (
"```python\ny = x * 2\nFINAL(y)\n```"
),
"content": ("```python\ny = x * 2\nFINAL(y)\n```"),
"usage": {
"prompt_tokens": 20,
"completion_tokens": 10,
@@ -335,9 +328,7 @@ class TestRLMSubLMWithTools:
# Root LM: code that calls llm_query
{
"content": (
"```python\n"
"result = llm_query('Calculate 2+2')\n"
"FINAL(result)\n```"
"```python\nresult = llm_query('Calculate 2+2')\nFINAL(result)\n```"
),
"usage": {
"prompt_tokens": 5,
+1 -4
View File
@@ -91,10 +91,7 @@ class TestSimpleAgent:
agent = SimpleAgent(engine, "test-model", bus=bus)
agent.run("test input")
evts = bus.history
start = [
e for e in evts
if e.event_type == EventType.AGENT_TURN_START
][0]
start = [e for e in evts if e.event_type == EventType.AGENT_TURN_START][0]
assert start.data["agent"] == "simple"
assert start.data["input"] == "test input"
+35 -20
View File
@@ -18,10 +18,13 @@ def test_activity_tracking_updates_last_activity_at(tmp_path):
agent = mgr.create_agent("stall-test")
def fake_invoke(agent_dict):
bus.publish(EventType.TOOL_CALL_START, {
"agent": agent_dict["id"],
"tool": "web_search",
})
bus.publish(
EventType.TOOL_CALL_START,
{
"agent": agent_dict["id"],
"tool": "web_search",
},
)
return AgentResult(content="done", metadata={})
with patch.object(executor, "_invoke_agent", side_effect=fake_invoke):
@@ -44,10 +47,13 @@ def test_activity_tracking_filters_by_agent_id(tmp_path):
def fake_invoke(agent_dict):
# Emit event for agent_b while agent_a is executing
bus.publish(EventType.TOOL_CALL_START, {
"agent": agent_b["id"],
"tool": "web_search",
})
bus.publish(
EventType.TOOL_CALL_START,
{
"agent": agent_b["id"],
"tool": "web_search",
},
)
return AgentResult(content="done", metadata={})
with patch.object(executor, "_invoke_agent", side_effect=fake_invoke):
@@ -67,10 +73,13 @@ def test_reconcile_detects_stalled_agent(tmp_path):
scheduler = AgentScheduler(mgr, executor, event_bus=bus)
agent = mgr.create_agent("stall-me", config={
"timeout_seconds": 10,
"max_stall_retries": 3,
})
agent = mgr.create_agent(
"stall-me",
config={
"timeout_seconds": 10,
"max_stall_retries": 3,
},
)
mgr.update_agent(agent["id"], status="running", last_activity_at=time.time() - 30)
scheduler._reconcile()
@@ -79,8 +88,7 @@ def test_reconcile_detects_stalled_agent(tmp_path):
assert updated["stall_retries"] == 1
stall_events = [
e for e in bus.history
if e.event_type == EventType.AGENT_STALL_DETECTED
e for e in bus.history if e.event_type == EventType.AGENT_STALL_DETECTED
]
assert len(stall_events) == 1
mgr.close()
@@ -114,12 +122,19 @@ def test_reconcile_retries_exhausted_sets_error(tmp_path):
scheduler = AgentScheduler(mgr, executor, event_bus=bus)
agent = mgr.create_agent("exhausted", config={
"timeout_seconds": 10,
"max_stall_retries": 2,
})
mgr.update_agent(agent["id"], status="running",
last_activity_at=time.time() - 30, stall_retries=2)
agent = mgr.create_agent(
"exhausted",
config={
"timeout_seconds": 10,
"max_stall_retries": 2,
},
)
mgr.update_agent(
agent["id"],
status="running",
last_activity_at=time.time() - 30,
stall_retries=2,
)
scheduler._reconcile()
+1
View File
@@ -1,4 +1,5 @@
"""Tests for suggest_action helper."""
from openjarvis.agents.errors import FatalError, RetryableError, suggest_action
+20 -12
View File
@@ -21,17 +21,23 @@ def test_executor_records_trace(tmp_path):
agent = mgr.create_agent("trace-test")
def fake_invoke(agent_dict):
bus.publish(EventType.TOOL_CALL_START, {
"agent": agent_dict["id"],
"tool": "web_search",
"args": {"query": "test"},
})
bus.publish(EventType.TOOL_CALL_END, {
"agent": agent_dict["id"],
"tool": "web_search",
"result": "search results...",
"duration": 0.5,
})
bus.publish(
EventType.TOOL_CALL_START,
{
"agent": agent_dict["id"],
"tool": "web_search",
"args": {"query": "test"},
},
)
bus.publish(
EventType.TOOL_CALL_END,
{
"agent": agent_dict["id"],
"tool": "web_search",
"result": "search results...",
"duration": 0.5,
},
)
return AgentResult(content="found it", metadata={"tokens_used": 100})
with patch.object(executor, "_invoke_agent", side_effect=fake_invoke):
@@ -62,7 +68,9 @@ def test_executor_records_error_trace(tmp_path):
agent = mgr.create_agent("error-trace")
with patch.object(
executor, "_invoke_agent", side_effect=FatalError("boom"),
executor,
"_invoke_agent",
side_effect=FatalError("boom"),
):
executor.execute_tick(agent["id"])
+4 -1
View File
@@ -78,7 +78,10 @@ class TestEnergyBenchmark:
b = EnergyBenchmark()
result = b.run(
engine, "test-model", num_samples=3, warmup_samples=0,
engine,
"test-model",
num_samples=3,
warmup_samples=0,
energy_monitor=monitor,
)
+6 -2
View File
@@ -56,8 +56,12 @@ class TestLatencyBenchmark:
b = LatencyBenchmark()
result = b.run(engine, "test-model", num_samples=3)
expected_keys = {
"mean_latency", "p50_latency", "p95_latency",
"min_latency", "max_latency", "std_latency",
"mean_latency",
"p50_latency",
"p95_latency",
"min_latency",
"max_latency",
"std_latency",
}
assert set(result.metrics.keys()) == expected_keys
+3 -1
View File
@@ -66,7 +66,9 @@ class TestChannelConfig:
class TestTomlLoading:
def _write_toml(self, content: str) -> Path:
f = tempfile.NamedTemporaryFile(
mode="w", suffix=".toml", delete=False,
mode="w",
suffix=".toml",
delete=False,
)
f.write(content)
f.flush()
+18 -6
View File
@@ -25,8 +25,12 @@ class TestChannelRegistry:
pass
def send(
self, channel, content,
*, conversation_id="", metadata=None,
self,
channel,
content,
*,
conversation_id="",
metadata=None,
) -> bool:
return True
@@ -56,8 +60,12 @@ class TestChannelRegistry:
pass
def send(
self, channel, content,
*, conversation_id="", metadata=None,
self,
channel,
content,
*,
conversation_id="",
metadata=None,
) -> bool:
return True
@@ -99,8 +107,12 @@ class TestChannelRegistry:
pass
def send(
self, channel, content,
*, conversation_id="", metadata=None,
self,
channel,
content,
*,
conversation_id="",
metadata=None,
) -> bool:
return True
+91 -55
View File
@@ -153,10 +153,13 @@ class TestChannelImportError:
class TestLineChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"LINE_CHANNEL_ACCESS_TOKEN": "env-tok",
"LINE_CHANNEL_SECRET": "env-sec",
}):
with patch.dict(
"os.environ",
{
"LINE_CHANNEL_ACCESS_TOKEN": "env-tok",
"LINE_CHANNEL_SECRET": "env-sec",
},
):
ch = LineChannel()
assert ch._channel_access_token == "env-tok"
assert ch._channel_secret == "env-sec"
@@ -164,10 +167,13 @@ class TestLineChannel:
class TestViberChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"VIBER_AUTH_TOKEN": "env-tok",
"VIBER_BOT_NAME": "TestBot",
}):
with patch.dict(
"os.environ",
{
"VIBER_AUTH_TOKEN": "env-tok",
"VIBER_BOT_NAME": "TestBot",
},
):
ch = ViberChannel()
assert ch._auth_token == "env-tok"
assert ch._name == "TestBot"
@@ -175,21 +181,27 @@ class TestViberChannel:
class TestMessengerChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"MESSENGER_ACCESS_TOKEN": "env-tok",
}):
with patch.dict(
"os.environ",
{
"MESSENGER_ACCESS_TOKEN": "env-tok",
},
):
ch = MessengerChannel()
assert ch._access_token == "env-tok"
class TestRedditChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"REDDIT_CLIENT_ID": "cid",
"REDDIT_CLIENT_SECRET": "csec",
"REDDIT_USERNAME": "user",
"REDDIT_PASSWORD": "pass",
}):
with patch.dict(
"os.environ",
{
"REDDIT_CLIENT_ID": "cid",
"REDDIT_CLIENT_SECRET": "csec",
"REDDIT_USERNAME": "user",
"REDDIT_PASSWORD": "pass",
},
):
ch = RedditChannel()
assert ch._client_id == "cid"
assert ch._client_secret == "csec"
@@ -199,10 +211,13 @@ class TestRedditChannel:
class TestMastodonChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"MASTODON_API_BASE_URL": "https://m.social",
"MASTODON_ACCESS_TOKEN": "env-tok",
}):
with patch.dict(
"os.environ",
{
"MASTODON_API_BASE_URL": "https://m.social",
"MASTODON_ACCESS_TOKEN": "env-tok",
},
):
ch = MastodonChannel()
assert ch._api_base_url == "https://m.social"
assert ch._access_token == "env-tok"
@@ -210,12 +225,15 @@ class TestMastodonChannel:
class TestXMPPChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"XMPP_JID": "bot@example.com",
"XMPP_PASSWORD": "pass",
"XMPP_SERVER": "xmpp.example.com",
"XMPP_PORT": "5223",
}):
with patch.dict(
"os.environ",
{
"XMPP_JID": "bot@example.com",
"XMPP_PASSWORD": "pass",
"XMPP_SERVER": "xmpp.example.com",
"XMPP_PORT": "5223",
},
):
ch = XMPPChannel()
assert ch._jid == "bot@example.com"
assert ch._password == "pass"
@@ -225,22 +243,28 @@ class TestXMPPChannel:
class TestRocketChatChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"ROCKETCHAT_URL": "https://rc.example.com",
"ROCKETCHAT_USER": "bot",
"ROCKETCHAT_PASSWORD": "pass",
}):
with patch.dict(
"os.environ",
{
"ROCKETCHAT_URL": "https://rc.example.com",
"ROCKETCHAT_USER": "bot",
"ROCKETCHAT_PASSWORD": "pass",
},
):
ch = RocketChatChannel()
assert ch._url == "https://rc.example.com"
assert ch._user == "bot"
assert ch._password == "pass"
def test_token_auth_env(self):
with patch.dict("os.environ", {
"ROCKETCHAT_URL": "https://rc.example.com",
"ROCKETCHAT_AUTH_TOKEN": "tok",
"ROCKETCHAT_USER_ID": "uid",
}):
with patch.dict(
"os.environ",
{
"ROCKETCHAT_URL": "https://rc.example.com",
"ROCKETCHAT_AUTH_TOKEN": "tok",
"ROCKETCHAT_USER_ID": "uid",
},
):
ch = RocketChatChannel()
assert ch._auth_token == "tok"
assert ch._user_id == "uid"
@@ -248,32 +272,41 @@ class TestRocketChatChannel:
class TestZulipChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"ZULIP_EMAIL": "bot@zulip.com",
"ZULIP_API_KEY": "key",
"ZULIP_SITE": "https://z.com",
}):
with patch.dict(
"os.environ",
{
"ZULIP_EMAIL": "bot@zulip.com",
"ZULIP_API_KEY": "key",
"ZULIP_SITE": "https://z.com",
},
):
ch = ZulipChannel()
assert ch._email == "bot@zulip.com"
assert ch._api_key == "key"
assert ch._site == "https://z.com"
def test_zuliprc_env(self):
with patch.dict("os.environ", {
"ZULIP_RC": "/path/to/zuliprc",
}):
with patch.dict(
"os.environ",
{
"ZULIP_RC": "/path/to/zuliprc",
},
):
ch = ZulipChannel()
assert ch._zuliprc == "/path/to/zuliprc"
class TestTwitchChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"TWITCH_ACCESS_TOKEN": "env-tok",
"TWITCH_CLIENT_ID": "env-cid",
"TWITCH_NICK": "env-nick",
"TWITCH_CHANNELS": "chan1,chan2",
}):
with patch.dict(
"os.environ",
{
"TWITCH_ACCESS_TOKEN": "env-tok",
"TWITCH_CLIENT_ID": "env-cid",
"TWITCH_NICK": "env-nick",
"TWITCH_CHANNELS": "chan1,chan2",
},
):
ch = TwitchChannel()
assert ch._access_token == "env-tok"
assert ch._client_id == "env-cid"
@@ -283,10 +316,13 @@ class TestTwitchChannel:
class TestNostrChannel:
def test_env_fallback(self):
with patch.dict("os.environ", {
"NOSTR_PRIVATE_KEY": "aa" * 32,
"NOSTR_RELAYS": "wss://r1.example.com,wss://r2.example.com",
}):
with patch.dict(
"os.environ",
{
"NOSTR_PRIVATE_KEY": "aa" * 32,
"NOSTR_RELAYS": "wss://r1.example.com,wss://r2.example.com",
},
):
ch = NostrChannel()
assert ch._private_key == "aa" * 32
assert len(ch._relays) == 2
+11 -6
View File
@@ -26,7 +26,8 @@ class TestRegistration:
def test_channel_id(self):
ch = EmailChannel(
smtp_host="smtp.example.com", username="user@example.com",
smtp_host="smtp.example.com",
username="user@example.com",
)
assert ch.channel_id == "email"
@@ -62,10 +63,13 @@ class TestInit:
assert ch._use_tls is False
def test_env_var_fallback(self):
with patch.dict(os.environ, {
"EMAIL_USERNAME": "env@example.com",
"EMAIL_PASSWORD": "env-pass",
}):
with patch.dict(
os.environ,
{
"EMAIL_USERNAME": "env@example.com",
"EMAIL_PASSWORD": "env-pass",
},
):
ch = EmailChannel()
assert ch._username == "env@example.com"
assert ch._password == "env-pass"
@@ -206,7 +210,8 @@ class TestOnMessage:
class TestDisconnect:
def test_disconnect(self):
ch = EmailChannel(
smtp_host="smtp.example.com", username="user@example.com",
smtp_host="smtp.example.com",
username="user@example.com",
)
ch._status = ChannelStatus.CONNECTED
ch.disconnect()
+54 -19
View File
@@ -25,7 +25,9 @@ class TestRegistration:
assert ChannelRegistry.contains("google_chat")
def test_channel_id(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
assert ch.channel_id == "google_chat"
@@ -36,27 +38,48 @@ class TestInit:
assert ch._status == ChannelStatus.DISCONNECTED
def test_constructor_url(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
assert ch._webhook_url == "https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
assert (
ch._webhook_url
== "https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
def test_env_var_fallback(self):
with patch.dict(os.environ, {
"GOOGLE_CHAT_WEBHOOK_URL": "https://chat.googleapis.com/v1/spaces/env/messages?key=env",
}):
with patch.dict(
os.environ,
{
"GOOGLE_CHAT_WEBHOOK_URL": "https://chat.googleapis.com/v1/spaces/env/messages?key=env",
},
):
ch = GoogleChatChannel()
assert ch._webhook_url == "https://chat.googleapis.com/v1/spaces/env/messages?key=env"
assert (
ch._webhook_url
== "https://chat.googleapis.com/v1/spaces/env/messages?key=env"
)
def test_constructor_overrides_env(self):
with patch.dict(os.environ, {
"GOOGLE_CHAT_WEBHOOK_URL": "https://chat.googleapis.com/v1/spaces/env/messages?key=env",
}):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/explicit/messages?key=explicit")
assert ch._webhook_url == "https://chat.googleapis.com/v1/spaces/explicit/messages?key=explicit"
with patch.dict(
os.environ,
{
"GOOGLE_CHAT_WEBHOOK_URL": "https://chat.googleapis.com/v1/spaces/env/messages?key=env",
},
):
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/explicit/messages?key=explicit"
)
assert (
ch._webhook_url
== "https://chat.googleapis.com/v1/spaces/explicit/messages?key=explicit"
)
class TestSend:
def test_send_success(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
mock_response = MagicMock()
mock_response.status_code = 200
@@ -67,7 +90,9 @@ class TestSend:
mock_post.assert_called_once()
def test_send_failure(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
mock_response = MagicMock()
mock_response.status_code = 400
@@ -78,7 +103,9 @@ class TestSend:
assert result is False
def test_send_exception(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
with patch("httpx.post", side_effect=ConnectionError("refused")):
result = ch.send("space", "Hello!")
@@ -108,13 +135,17 @@ class TestSend:
class TestListChannels:
def test_list_channels(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
assert ch.list_channels() == ["google_chat"]
class TestStatus:
def test_disconnected_initially(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
assert ch.status() == ChannelStatus.DISCONNECTED
def test_no_url_connect_error(self):
@@ -125,7 +156,9 @@ class TestStatus:
class TestOnMessage:
def test_on_message(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
handler = MagicMock()
ch.on_message(handler)
assert handler in ch._handlers
@@ -133,7 +166,9 @@ class TestOnMessage:
class TestDisconnect:
def test_disconnect(self):
ch = GoogleChatChannel(webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy")
ch = GoogleChatChannel(
webhook_url="https://chat.googleapis.com/v1/spaces/xxx/messages?key=yyy"
)
ch._status = ChannelStatus.CONNECTED
ch.disconnect()
assert ch.status() == ChannelStatus.DISCONNECTED
+17 -11
View File
@@ -45,12 +45,15 @@ class TestInit:
assert ch._password == "pass123"
def test_env_var_fallback(self):
with patch.dict(os.environ, {
"IRC_SERVER": "irc.env.com",
"IRC_NICK": "envbot",
"IRC_PASSWORD": "envpass",
"IRC_PORT": "6697",
}):
with patch.dict(
os.environ,
{
"IRC_SERVER": "irc.env.com",
"IRC_NICK": "envbot",
"IRC_PASSWORD": "envpass",
"IRC_PORT": "6697",
},
):
ch = IRCChannel()
assert ch._server == "irc.env.com"
assert ch._nick == "envbot"
@@ -58,11 +61,14 @@ class TestInit:
assert ch._port == 6697
def test_constructor_overrides_env(self):
with patch.dict(os.environ, {
"IRC_SERVER": "irc.env.com",
"IRC_NICK": "envbot",
"IRC_PASSWORD": "envpass",
}):
with patch.dict(
os.environ,
{
"IRC_SERVER": "irc.env.com",
"IRC_NICK": "envbot",
"IRC_PASSWORD": "envpass",
},
):
ch = IRCChannel(
server="irc.explicit.com",
nick="explicit",
+14 -8
View File
@@ -42,19 +42,25 @@ class TestInit:
assert ch._phone_number == "+1234567890"
def test_env_var_fallback(self):
with patch.dict(os.environ, {
"SIGNAL_API_URL": "http://env-signal:8080",
"SIGNAL_PHONE_NUMBER": "+9876543210",
}):
with patch.dict(
os.environ,
{
"SIGNAL_API_URL": "http://env-signal:8080",
"SIGNAL_PHONE_NUMBER": "+9876543210",
},
):
ch = SignalChannel()
assert ch._api_url == "http://env-signal:8080"
assert ch._phone_number == "+9876543210"
def test_constructor_overrides_env(self):
with patch.dict(os.environ, {
"SIGNAL_API_URL": "http://env-signal:8080",
"SIGNAL_PHONE_NUMBER": "+9876543210",
}):
with patch.dict(
os.environ,
{
"SIGNAL_API_URL": "http://env-signal:8080",
"SIGNAL_PHONE_NUMBER": "+9876543210",
},
):
ch = SignalChannel(
api_url="http://explicit:8080",
phone_number="+1111111111",
+6 -2
View File
@@ -95,8 +95,12 @@ class TestBaseChannel:
pass
def send(
self, channel, content,
*, conversation_id="", metadata=None,
self,
channel,
content,
*,
conversation_id="",
metadata=None,
) -> bool:
return True
+122
View File
@@ -140,3 +140,125 @@ class TestDisconnect:
ch._status = ChannelStatus.CONNECTED
ch.disconnect()
assert ch.status() == ChannelStatus.DISCONNECTED
class TestAllowedChatIds:
"""Tests for the allowed_chat_ids enforcement in _poll_loop."""
def _make_update(self, chat_id: str, text: str = "hello"):
"""Build a minimal fake python-telegram-bot Update object."""
msg = MagicMock()
msg.text = text
msg.message_id = 1
msg.from_user.id = chat_id
msg.chat.id = chat_id
update = MagicMock()
update.message = msg
return update
def _invoke_handle_msg(self, ch: TelegramChannel, chat_id: str, text: str = "hi"):
"""Simulate _poll_loop dispatching a message without starting a thread."""
from openjarvis.channels._stubs import ChannelMessage
cm = ChannelMessage(
channel="telegram",
sender=chat_id,
content=text,
message_id="1",
conversation_id=chat_id,
)
# Directly exercise the allow-list logic (mirrors _handle_msg body)
if ch._allowed_chat_ids:
_allowed = {
cid.strip() for cid in ch._allowed_chat_ids.split(",") if cid.strip()
}
if cm.conversation_id not in _allowed:
return False # would return inside _handle_msg
for handler in ch._handlers:
handler(cm)
return True
def test_no_allowlist_accepts_any(self):
"""When allowed_chat_ids is empty every chat is dispatched."""
ch = TelegramChannel(bot_token="tok", allowed_chat_ids="")
handler = MagicMock()
ch.on_message(handler)
dispatched = self._invoke_handle_msg(ch, "99999")
assert dispatched is True
handler.assert_called_once()
def test_allowlist_passes_listed_chat(self):
"""A chat ID present in the allow-list is dispatched to handlers."""
ch = TelegramChannel(bot_token="tok", allowed_chat_ids="111,222")
handler = MagicMock()
ch.on_message(handler)
dispatched = self._invoke_handle_msg(ch, "111")
assert dispatched is True
handler.assert_called_once()
def test_allowlist_blocks_unlisted_chat(self):
"""A chat ID not in the allow-list is silently dropped (not dispatched)."""
ch = TelegramChannel(bot_token="tok", allowed_chat_ids="111,222")
handler = MagicMock()
ch.on_message(handler)
dispatched = self._invoke_handle_msg(ch, "999")
assert dispatched is False
handler.assert_not_called()
def test_allowlist_trims_whitespace(self):
"""Spaces around IDs in the allow-list are handled gracefully."""
ch = TelegramChannel(bot_token="tok", allowed_chat_ids=" 111 , 222 ")
handler = MagicMock()
ch.on_message(handler)
dispatched = self._invoke_handle_msg(ch, "111")
assert dispatched is True
handler.assert_called_once()
class TestChannelAgentWiring:
"""Tests for the channel → agent handler wired in serve.py."""
def test_on_message_handler_invoked_on_message(self):
"""on_message callback registered on a channel is called when a message
arrives."""
ch = TelegramChannel(bot_token="tok")
received = []
ch.on_message(lambda cm: received.append(cm))
from openjarvis.channels._stubs import ChannelMessage
cm = ChannelMessage(
channel="telegram",
sender="42",
content="ping",
message_id="1",
conversation_id="42",
)
for h in ch._handlers:
h(cm)
assert len(received) == 1
assert received[0].content == "ping"
def test_multiple_handlers_all_invoked(self):
"""Both handlers registered via on_message are called for the same message."""
ch = TelegramChannel(bot_token="tok")
calls_a: list = []
calls_b: list = []
ch.on_message(lambda cm: calls_a.append(cm))
ch.on_message(lambda cm: calls_b.append(cm))
from openjarvis.channels._stubs import ChannelMessage
cm = ChannelMessage(
channel="telegram",
sender="1",
content="x",
message_id="1",
conversation_id="1",
)
for h in ch._handlers:
h(cm)
assert len(calls_a) == 1
assert len(calls_b) == 1
+14 -8
View File
@@ -42,19 +42,25 @@ class TestInit:
assert ch._phone_number_id == "12345"
def test_env_var_fallback(self):
with patch.dict(os.environ, {
"WHATSAPP_ACCESS_TOKEN": "env-token",
"WHATSAPP_PHONE_NUMBER_ID": "env-id",
}):
with patch.dict(
os.environ,
{
"WHATSAPP_ACCESS_TOKEN": "env-token",
"WHATSAPP_PHONE_NUMBER_ID": "env-id",
},
):
ch = WhatsAppChannel()
assert ch._token == "env-token"
assert ch._phone_number_id == "env-id"
def test_constructor_overrides_env(self):
with patch.dict(os.environ, {
"WHATSAPP_ACCESS_TOKEN": "env-token",
"WHATSAPP_PHONE_NUMBER_ID": "env-id",
}):
with patch.dict(
os.environ,
{
"WHATSAPP_ACCESS_TOKEN": "env-token",
"WHATSAPP_PHONE_NUMBER_ID": "env-id",
},
):
ch = WhatsAppChannel(
access_token="explicit-token",
phone_number_id="explicit-id",
+28 -21
View File
@@ -259,13 +259,15 @@ class TestReaderLoop:
bus = EventBus(record_history=True)
ch = WhatsAppBaileysChannel(bus=bus)
ch._handle_bridge_event({
"type": "message",
"jid": "123@s.whatsapp.net",
"sender": "456@s.whatsapp.net",
"text": "Bus test",
"message_id": "msg-002",
})
ch._handle_bridge_event(
{
"type": "message",
"jid": "123@s.whatsapp.net",
"sender": "456@s.whatsapp.net",
"text": "Bus test",
"message_id": "msg-002",
}
)
event_types = [e.event_type for e in bus.history]
assert EventType.CHANNEL_MESSAGE_RECEIVED in event_types
@@ -276,13 +278,16 @@ class TestReaderLoop:
lines = [
json.dumps({"type": "status", "status": "connected"}) + "\n",
json.dumps({
"type": "message",
"jid": "j",
"sender": "s",
"text": "t",
"message_id": "m",
}) + "\n",
json.dumps(
{
"type": "message",
"jid": "j",
"sender": "s",
"text": "t",
"message_id": "m",
}
)
+ "\n",
]
mock_proc = MagicMock()
@@ -316,11 +321,13 @@ class TestReaderLoop:
ch.on_message(bad_handler)
# Should not raise.
ch._handle_bridge_event({
"type": "message",
"jid": "j",
"sender": "s",
"text": "t",
"message_id": "m",
})
ch._handle_bridge_event(
{
"type": "message",
"jid": "j",
"sender": "s",
"text": "t",
"message_id": "m",
}
)
bad_handler.assert_called_once()
+5 -4
View File
@@ -42,7 +42,8 @@ class TestAddCmd:
mcp_dir = tmp_path / "mcp"
with mock.patch("openjarvis.cli.add_cmd._MCP_CONFIG_DIR", mcp_dir):
result = CliRunner().invoke(
add, ["github", "--key", "test_token"],
add,
["github", "--key", "test_token"],
)
assert result.exit_code == 0
@@ -55,6 +56,6 @@ class TestAddCmd:
def test_mcp_templates_complete(self) -> None:
required_fields = {"command", "args", "env_key", "description"}
for name, tmpl in _MCP_TEMPLATES.items():
assert required_fields.issubset(
tmpl.keys()
), f"Template '{name}' missing fields: {required_fields - tmpl.keys()}"
assert required_fields.issubset(tmpl.keys()), (
f"Template '{name}' missing fields: {required_fields - tmpl.keys()}"
)
+13 -2
View File
@@ -80,8 +80,19 @@ class TestNewAgentCommands:
result = CliRunner().invoke(cli, ["agents", "--help"])
assert result.exit_code == 0
cmds = (
"launch", "start", "stop", "run", "status", "logs",
"daemon", "watch", "recover", "errors", "ask", "instruct", "messages",
"launch",
"start",
"stop",
"run",
"status",
"logs",
"daemon",
"watch",
"recover",
"errors",
"ask",
"instruct",
"messages",
)
for cmd in cmds:
assert cmd in result.output, f"Missing command: {cmd}"
+20 -9
View File
@@ -123,7 +123,8 @@ def agent_setup():
patch.object(_ask_mod, "get_engine", return_value=("mock", engine)),
patch.object(_ask_mod, "discover_engines", return_value=[("mock", engine)]),
patch.object(
_ask_mod, "discover_models",
_ask_mod,
"discover_models",
return_value={"mock": ["test-model"]},
),
patch.object(_ask_mod, "register_builtin_models"),
@@ -152,6 +153,7 @@ def mock_setup():
patch.object(_ask_mod, "merge_discovered_models"),
):
from openjarvis.core.config import JarvisConfig
mock_cfg.return_value = JarvisConfig()
mock_ge.return_value = ("mock", engine)
mock_de.return_value = [("mock", engine)]
@@ -175,7 +177,8 @@ class TestAskAgentOption:
def test_agent_orchestrator_no_tools(self, runner, mock_setup):
result = runner.invoke(
cli, ["ask", "--agent", "orchestrator", "Hello"],
cli,
["ask", "--agent", "orchestrator", "Hello"],
)
assert result.exit_code == 0
@@ -183,8 +186,11 @@ class TestAskAgentOption:
result = runner.invoke(
cli,
[
"ask", "--agent", "orchestrator",
"--tools", "calculator,think",
"ask",
"--agent",
"orchestrator",
"--tools",
"calculator,think",
"What is 2+2?",
],
)
@@ -192,7 +198,8 @@ class TestAskAgentOption:
def test_agent_json_output(self, runner, mock_setup):
result = runner.invoke(
cli, ["ask", "--agent", "simple", "--json", "Hello"],
cli,
["ask", "--agent", "simple", "--json", "Hello"],
)
assert result.exit_code == 0
assert '"content"' in result.output
@@ -200,7 +207,8 @@ class TestAskAgentOption:
def test_unknown_agent(self, runner, mock_setup):
result = runner.invoke(
cli, ["ask", "--agent", "nonexistent", "Hello"],
cli,
["ask", "--agent", "nonexistent", "Hello"],
)
assert result.exit_code != 0
@@ -211,13 +219,15 @@ class TestAskAgentOption:
def test_agent_simple_with_model(self, runner, mock_setup):
result = runner.invoke(
cli, ["ask", "--agent", "simple", "-m", "test-model", "Hello"],
cli,
["ask", "--agent", "simple", "-m", "test-model", "Hello"],
)
assert result.exit_code == 0
def test_agent_simple_with_temperature(self, runner, mock_setup):
result = runner.invoke(
cli, ["ask", "--agent", "simple", "-t", "0.1", "Hello"],
cli,
["ask", "--agent", "simple", "-t", "0.1", "Hello"],
)
assert result.exit_code == 0
@@ -240,7 +250,8 @@ class TestAskAgentOption:
agent_setup.config.agent.tools = agent_tools
result = runner.invoke(
cli, ["ask", "--agent", "confirming_agent", "Hello"],
cli,
["ask", "--agent", "confirming_agent", "Hello"],
)
assert result.exit_code == 0
+5 -5
View File
@@ -11,9 +11,7 @@ from openjarvis.cli import cli
def test_ask_no_context_flag():
"""The --no-context flag is accepted."""
result = CliRunner().invoke(
cli, ["ask", "--no-context", "--help"]
)
result = CliRunner().invoke(cli, ["ask", "--no-context", "--help"])
# --help should succeed regardless
assert result.exit_code == 0
@@ -26,7 +24,8 @@ def test_ask_has_no_context_option():
def test_get_memory_backend_returns_none_when_empty(
tmp_path, monkeypatch,
tmp_path,
monkeypatch,
):
"""_get_memory_backend returns None when no docs indexed."""
from openjarvis.core.config import JarvisConfig, MemoryConfig
@@ -47,7 +46,8 @@ def test_get_memory_backend_returns_none_when_empty(
def test_get_memory_backend_returns_backend_with_docs(
tmp_path, monkeypatch,
tmp_path,
monkeypatch,
):
"""_get_memory_backend returns a backend when docs exist."""
from openjarvis.core.config import JarvisConfig, MemoryConfig
+18 -39
View File
@@ -30,9 +30,7 @@ def _mock_engine_response():
}
def _patch_ask(
monkeypatch, tmp_path, *, engine_result=None, no_engine=False
):
def _patch_ask(monkeypatch, tmp_path, *, engine_result=None, no_engine=False):
"""Set up common mocks for ask tests."""
cfg = JarvisConfig()
cfg.telemetry.db_path = str(tmp_path / "telemetry.db")
@@ -40,74 +38,55 @@ def _patch_ask(
monkeypatch.setattr(_ask_mod, "load_config", lambda: cfg)
if no_engine:
monkeypatch.setattr(
_ask_mod, "get_engine", lambda *a, **kw: None
)
monkeypatch.setattr(_ask_mod, "get_engine", lambda *a, **kw: None)
else:
fake_engine = mock.MagicMock()
fake_engine.engine_id = "mock"
fake_engine.health.return_value = True
fake_engine.generate.return_value = (
engine_result or _mock_engine_response()
)
fake_engine.generate.return_value = engine_result or _mock_engine_response()
fake_engine.list_models.return_value = ["test-model"]
monkeypatch.setattr(
_ask_mod, "get_engine",
_ask_mod,
"get_engine",
lambda *a, **kw: ("mock", fake_engine),
)
monkeypatch.setattr(
_ask_mod, "discover_engines",
_ask_mod,
"discover_engines",
lambda c: [("mock", fake_engine)],
)
monkeypatch.setattr(
_ask_mod, "discover_models",
_ask_mod,
"discover_models",
lambda e: {"mock": ["test-model"]},
)
class TestAskCommand:
def test_basic_response(
self, monkeypatch, tmp_path: Path
) -> None:
def test_basic_response(self, monkeypatch, tmp_path: Path) -> None:
_patch_ask(monkeypatch, tmp_path)
result = CliRunner().invoke(
cli, ["ask", "What is 2+2?"]
)
result = CliRunner().invoke(cli, ["ask", "What is 2+2?"])
assert result.exit_code == 0
assert "The answer is 4" in result.output
def test_no_engine_error(
self, monkeypatch, tmp_path: Path
) -> None:
def test_no_engine_error(self, monkeypatch, tmp_path: Path) -> None:
_patch_ask(monkeypatch, tmp_path, no_engine=True)
result = CliRunner().invoke(
cli, ["ask", "Hello"]
)
result = CliRunner().invoke(cli, ["ask", "Hello"])
assert result.exit_code != 0
def test_model_override(
self, monkeypatch, tmp_path: Path
) -> None:
def test_model_override(self, monkeypatch, tmp_path: Path) -> None:
_patch_ask(monkeypatch, tmp_path)
result = CliRunner().invoke(
cli, ["ask", "-m", "custom-model", "Hello"]
)
result = CliRunner().invoke(cli, ["ask", "-m", "custom-model", "Hello"])
assert result.exit_code == 0
def test_json_output(
self, monkeypatch, tmp_path: Path
) -> None:
def test_json_output(self, monkeypatch, tmp_path: Path) -> None:
_patch_ask(monkeypatch, tmp_path)
result = CliRunner().invoke(
cli, ["ask", "--json", "Hello"]
)
result = CliRunner().invoke(cli, ["ask", "--json", "Hello"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "content" in data
def test_telemetry_recorded(
self, monkeypatch, tmp_path: Path
) -> None:
def test_telemetry_recorded(self, monkeypatch, tmp_path: Path) -> None:
_patch_ask(monkeypatch, tmp_path)
CliRunner().invoke(cli, ["ask", "Hello"])
db_path = tmp_path / "telemetry.db"
+25 -10
View File
@@ -31,15 +31,18 @@ def _patch_engine(engine):
"""Return context managers that patch engine discovery to use our mock."""
return (
mock.patch.object(
_ask_mod, "get_engine",
_ask_mod,
"get_engine",
return_value=("mock", engine),
),
mock.patch.object(
_ask_mod, "discover_engines",
_ask_mod,
"discover_engines",
return_value={"mock": engine},
),
mock.patch.object(
_ask_mod, "discover_models",
_ask_mod,
"discover_models",
return_value={"mock": ["test-model"]},
),
mock.patch.object(_ask_mod, "register_builtin_models"),
@@ -64,7 +67,8 @@ class TestAskModelResolution:
patches = _patch_engine(engine)
with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5]:
result = CliRunner().invoke(
cli, ["ask", "-m", "test-model", "Hello"],
cli,
["ask", "-m", "test-model", "Hello"],
)
assert result.exit_code == 0
assert "Hello!" in result.output
@@ -74,9 +78,15 @@ class TestAskModelResolution:
engine = _mock_engine()
patches = _patch_engine(engine)
with (
patches[0], patches[1], patches[2], patches[3], patches[4], patches[5],
patches[0],
patches[1],
patches[2],
patches[3],
patches[4],
patches[5],
mock.patch.object(
_ask_mod, "load_config",
_ask_mod,
"load_config",
) as mock_config,
):
cfg = mock_config.return_value
@@ -93,14 +103,19 @@ class TestAskModelResolution:
patches = _patch_engine(engine)
# Override discover_models to return empty list
with (
patches[0], patches[1],
patches[0],
patches[1],
mock.patch.object(
_ask_mod, "discover_models",
_ask_mod,
"discover_models",
return_value={"mock": []},
),
patches[3], patches[4], patches[5],
patches[3],
patches[4],
patches[5],
mock.patch.object(
_ask_mod, "load_config",
_ask_mod,
"load_config",
) as mock_config,
):
cfg = mock_config.return_value
+4 -2
View File
@@ -56,7 +56,8 @@ class TestBenchCLI:
return_value=("mock", engine),
):
result = CliRunner().invoke(
cli, ["bench", "run", "-n", "2", "--json"],
cli,
["bench", "run", "-n", "2", "--json"],
)
assert result.exit_code == 0
assert "benchmark_count" in result.output
@@ -76,7 +77,8 @@ class TestBenchCLI:
return_value=("mock", engine),
):
result = CliRunner().invoke(
cli, ["bench", "run", "-n", "2", "-o", str(out_file)],
cli,
["bench", "run", "-n", "2", "-o", str(out_file)],
)
assert result.exit_code == 0
assert out_file.exists()
+6 -3
View File
@@ -25,7 +25,8 @@ def _patch_channel(
bridge_instance.status.return_value = status_return
config_patch = mock.patch(
"openjarvis.core.config.load_config", return_value=cfg,
"openjarvis.core.config.load_config",
return_value=cfg,
)
get_channel_patch = mock.patch(
"openjarvis.cli.channel_cmd._get_channel",
@@ -75,7 +76,8 @@ class TestChannelSend:
config_p, getch_p, _ = _patch_channel(send_return=True)
with config_p, getch_p:
result = CliRunner().invoke(
cli, ["channel", "send", "slack", "Hello!"],
cli,
["channel", "send", "slack", "Hello!"],
)
assert result.exit_code == 0
assert "Message sent" in result.output
@@ -84,7 +86,8 @@ class TestChannelSend:
config_p, getch_p, _ = _patch_channel(send_return=False)
with config_p, getch_p:
result = CliRunner().invoke(
cli, ["channel", "send", "slack", "Hello!"],
cli,
["channel", "send", "slack", "Hello!"],
)
assert result.exit_code == 0
assert "Failed to send" in result.output
+3 -11
View File
@@ -31,11 +31,7 @@ class TestCLI:
# Either exits with error (no engine) or succeeds (deps missing)
# Both are valid states for testing
out = result.output.lower()
assert (
result.exit_code != 0
or "not installed" in out
or "no inference" in out
)
assert result.exit_code != 0 or "not installed" in out or "no inference" in out
def test_model_subcommands_exist(self) -> None:
result = CliRunner().invoke(cli, ["model", "--help"])
@@ -82,12 +78,8 @@ class TestCLI:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
):
result = CliRunner().invoke(cli, ["init", "--engine", "ollama"])
assert result.exit_code == 0
+5 -15
View File
@@ -23,18 +23,14 @@ class TestDaemonCommands:
def test_stop_no_server(self) -> None:
"""``jarvis stop`` when no PID file shows 'not running'."""
with patch(
"openjarvis.cli.daemon_cmd._read_pid", return_value=None
):
with patch("openjarvis.cli.daemon_cmd._read_pid", return_value=None):
result = CliRunner().invoke(cli, ["stop"])
assert result.exit_code != 0
assert "No running server" in result.output
def test_status_no_server(self) -> None:
"""``jarvis status`` when no PID file shows 'not running'."""
with patch(
"openjarvis.cli.daemon_cmd._read_pid", return_value=None
):
with patch("openjarvis.cli.daemon_cmd._read_pid", return_value=None):
result = CliRunner().invoke(cli, ["status"])
assert result.exit_code == 0
assert "not running" in result.output
@@ -52,9 +48,7 @@ class TestDaemonCommands:
pid_file = tmp_path / "server.pid"
with (
patch("openjarvis.cli.daemon_cmd._PID_FILE", pid_file),
patch(
"openjarvis.cli.daemon_cmd.DEFAULT_CONFIG_DIR", tmp_path
),
patch("openjarvis.cli.daemon_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch("os.kill", return_value=None),
):
_write_pid(12345)
@@ -68,9 +62,7 @@ class TestDaemonCommands:
mock_config.server.port = 8000
with (
patch(
"openjarvis.cli.daemon_cmd._read_pid", return_value=9999
),
patch("openjarvis.cli.daemon_cmd._read_pid", return_value=9999),
patch(
"openjarvis.cli.daemon_cmd.load_config",
return_value=mock_config,
@@ -83,9 +75,7 @@ class TestDaemonCommands:
def test_start_already_running(self) -> None:
"""``jarvis start`` exits with error when a server is already running."""
with patch(
"openjarvis.cli.daemon_cmd._read_pid", return_value=42
):
with patch("openjarvis.cli.daemon_cmd._read_pid", return_value=42):
result = CliRunner().invoke(cli, ["start"])
assert result.exit_code != 0
assert "already running" in result.output
+8 -24
View File
@@ -32,19 +32,13 @@ class TestDoctorRuns:
mock_config.intelligence.default_model = ""
with (
patch(
"openjarvis.cli.doctor_cmd.load_config", return_value=mock_config
),
patch("openjarvis.cli.doctor_cmd.load_config", return_value=mock_config),
patch(
"openjarvis.cli.doctor_cmd.DEFAULT_CONFIG_PATH",
Path("/tmp/nonexistent/config.toml"),
),
patch(
"openjarvis.cli.doctor_cmd._check_engines", return_value=[]
),
patch(
"openjarvis.cli.doctor_cmd._check_models", return_value=[]
),
patch("openjarvis.cli.doctor_cmd._check_engines", return_value=[]),
patch("openjarvis.cli.doctor_cmd._check_models", return_value=[]),
):
result = CliRunner().invoke(cli, ["doctor"])
assert result.exit_code == 0
@@ -58,19 +52,13 @@ class TestDoctorJsonOutput:
mock_config.intelligence.default_model = ""
with (
patch(
"openjarvis.cli.doctor_cmd.load_config", return_value=mock_config
),
patch("openjarvis.cli.doctor_cmd.load_config", return_value=mock_config),
patch(
"openjarvis.cli.doctor_cmd.DEFAULT_CONFIG_PATH",
Path("/tmp/nonexistent/config.toml"),
),
patch(
"openjarvis.cli.doctor_cmd._check_engines", return_value=[]
),
patch(
"openjarvis.cli.doctor_cmd._check_models", return_value=[]
),
patch("openjarvis.cli.doctor_cmd._check_engines", return_value=[]),
patch("openjarvis.cli.doctor_cmd._check_models", return_value=[]),
):
result = CliRunner().invoke(cli, ["doctor", "--json"])
assert result.exit_code == 0
@@ -129,13 +117,9 @@ class TestCheckEngineProbing:
for key in sorted(keys):
engine = mock_make_engine(key, mock_config)
if engine.health():
results.append(
CheckResult(f"Engine: {key}", "ok", "Reachable")
)
results.append(CheckResult(f"Engine: {key}", "ok", "Reachable"))
else:
results.append(
CheckResult(f"Engine: {key}", "warn", "Unreachable")
)
results.append(CheckResult(f"Engine: {key}", "warn", "Unreachable"))
names = [r.name for r in results]
assert "Engine: ollama" in names
+4 -4
View File
@@ -15,10 +15,12 @@ _real_import = builtins.__import__
def _selective_import_blocker(*blocked: str):
"""Return an __import__ replacement that blocks specific packages."""
def _import(name, *args, **kwargs):
if name in blocked:
raise ImportError(f"mocked: {name} not installed")
return _real_import(name, *args, **kwargs)
return _import
@@ -45,8 +47,7 @@ class TestDoctorOptionalLabels:
result = runner.invoke(cli, ["doctor", "--json"])
data = json.loads(result.output)
apple_checks = [
c for c in data
if c["name"] == "Optional: Apple Silicon energy monitoring"
c for c in data if c["name"] == "Optional: Apple Silicon energy monitoring"
]
assert len(apple_checks) == 1
assert "Not installed (openjarvis[energy-apple])" == apple_checks[0]["message"]
@@ -59,8 +60,7 @@ class TestDoctorOptionalLabels:
result = runner.invoke(cli, ["doctor", "--json"])
data = json.loads(result.output)
nvidia_checks = [
c for c in data
if c["name"] == "Optional: NVIDIA energy monitoring"
c for c in data if c["name"] == "Optional: NVIDIA energy monitoring"
]
assert len(nvidia_checks) == 1
assert "Not installed (openjarvis[gpu-metrics])" == nvidia_checks[0]["message"]
+8 -24
View File
@@ -17,12 +17,8 @@ class TestInitShowsNextSteps:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
):
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp"])
assert result.exit_code == 0
@@ -35,12 +31,8 @@ class TestInitShowsNextSteps:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
):
result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp"])
assert result.exit_code == 0
@@ -98,12 +90,8 @@ class TestMinimalConfig:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
):
result = CliRunner().invoke(cli, ["init", "--engine", "ollama"])
assert result.exit_code == 0
@@ -119,12 +107,8 @@ class TestMinimalConfig:
config_dir = tmp_path / ".openjarvis"
config_path = config_dir / "config.toml"
with (
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
),
mock.patch(
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir),
mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path),
):
result = CliRunner().invoke(cli, ["init", "--full", "--engine", "ollama"])
assert result.exit_code == 0
+1 -4
View File
@@ -29,10 +29,7 @@ class TestSetupLogging:
log_file = tmp_path / "cli.log"
logger = setup_logging(verbose=True, quiet=False, log_file=log_file)
# Should have at least one file handler
file_handlers = [
h for h in logger.handlers
if hasattr(h, "baseFilename")
]
file_handlers = [h for h in logger.handlers if hasattr(h, "baseFilename")]
assert len(file_handlers) >= 1
# Clean up
for h in logger.handlers[:]:
+12 -16
View File
@@ -29,13 +29,12 @@ def test_memory_index_file(tmp_path: Path, monkeypatch):
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod, "_get_backend",
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(
cli, ["memory", "index", str(doc)]
)
result = CliRunner().invoke(cli, ["memory", "index", str(doc)])
assert result.exit_code == 0
assert "Indexed" in result.output or "chunk" in result.output
@@ -43,9 +42,7 @@ def test_memory_index_file(tmp_path: Path, monkeypatch):
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")]
)
result = CliRunner().invoke(cli, ["memory", "index", str(tmp_path / "nope")])
assert result.exit_code != 0
@@ -61,13 +58,12 @@ def test_memory_search_returns_results(tmp_path: Path, monkeypatch):
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod, "_get_backend",
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(
cli, ["memory", "search", "Python"]
)
result = CliRunner().invoke(cli, ["memory", "search", "Python"])
assert result.exit_code == 0
assert "Python" in result.output
backend.close()
@@ -82,13 +78,12 @@ def test_memory_search_no_results(tmp_path: Path, monkeypatch):
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod, "_get_backend",
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
result = CliRunner().invoke(
cli, ["memory", "search", "quantum supercollider"]
)
result = CliRunner().invoke(cli, ["memory", "search", "quantum supercollider"])
assert result.exit_code == 0
assert "No results" in result.output
backend.close()
@@ -104,7 +99,8 @@ def test_memory_stats_shows_count(tmp_path: Path, monkeypatch):
mod = importlib.import_module("openjarvis.cli.memory_cmd")
monkeypatch.setattr(
mod, "_get_backend",
mod,
"_get_backend",
lambda b=None: SQLiteMemory(db_path=db_path),
)
+11 -23
View File
@@ -29,11 +29,13 @@ class TestModelList:
monkeypatch.setattr(_model_mod, "load_config", lambda: cfg)
fake = _mock_engine()
monkeypatch.setattr(
_model_mod, "discover_engines",
_model_mod,
"discover_engines",
lambda c: [("mock", fake)],
)
monkeypatch.setattr(
_model_mod, "discover_models",
_model_mod,
"discover_models",
lambda e: {"mock": ["model-a", "model-b"]},
)
result = CliRunner().invoke(cli, ["model", "list"])
@@ -43,9 +45,7 @@ class TestModelList:
def test_no_engines_message(self, monkeypatch) -> None:
cfg = JarvisConfig()
monkeypatch.setattr(_model_mod, "load_config", lambda: cfg)
monkeypatch.setattr(
_model_mod, "discover_engines", lambda c: []
)
monkeypatch.setattr(_model_mod, "discover_engines", lambda c: [])
result = CliRunner().invoke(cli, ["model", "list"])
assert result.exit_code == 0
assert "No inference engines" in result.output
@@ -55,28 +55,16 @@ class TestModelInfo:
def test_info_known_model(self, monkeypatch) -> None:
cfg = JarvisConfig()
monkeypatch.setattr(_model_mod, "load_config", lambda: cfg)
monkeypatch.setattr(
_model_mod, "discover_engines", lambda c: []
)
monkeypatch.setattr(
_model_mod, "discover_models", lambda e: {}
)
result = CliRunner().invoke(
cli, ["model", "info", "qwen3:8b"]
)
monkeypatch.setattr(_model_mod, "discover_engines", lambda c: [])
monkeypatch.setattr(_model_mod, "discover_models", lambda e: {})
result = CliRunner().invoke(cli, ["model", "info", "qwen3:8b"])
assert result.exit_code == 0
assert "Qwen3 8B" in result.output
def test_unknown_model_not_found(self, monkeypatch) -> None:
cfg = JarvisConfig()
monkeypatch.setattr(_model_mod, "load_config", lambda: cfg)
monkeypatch.setattr(
_model_mod, "discover_engines", lambda c: []
)
monkeypatch.setattr(
_model_mod, "discover_models", lambda e: {}
)
result = CliRunner().invoke(
cli, ["model", "info", "nonexistent-model"]
)
monkeypatch.setattr(_model_mod, "discover_engines", lambda c: [])
monkeypatch.setattr(_model_mod, "discover_models", lambda e: {})
result = CliRunner().invoke(cli, ["model", "info", "nonexistent-model"])
assert result.exit_code != 0
+24 -47
View File
@@ -32,28 +32,23 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
"openjarvis.cli.quickstart_cmd.generate_default_toml",
return_value="[engine]\n",
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
"openjarvis.cli.quickstart_cmd.recommend_engine",
return_value="ollama",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
"openjarvis.cli.quickstart_cmd._check_engine_health",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_model_available",
"openjarvis.cli.quickstart_cmd._check_model_available",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._test_query",
"openjarvis.cli.quickstart_cmd._test_query",
return_value="Hello!",
),
):
@@ -79,28 +74,23 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
"openjarvis.cli.quickstart_cmd.generate_default_toml",
return_value="[engine]\n",
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
"openjarvis.cli.quickstart_cmd.recommend_engine",
return_value="ollama",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
"openjarvis.cli.quickstart_cmd._check_engine_health",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_model_available",
"openjarvis.cli.quickstart_cmd._check_model_available",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._test_query",
"openjarvis.cli.quickstart_cmd._test_query",
return_value="Hello!",
),
):
@@ -128,23 +118,19 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
"openjarvis.cli.quickstart_cmd.generate_default_toml",
return_value="[engine]\nnew = true\n",
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
"openjarvis.cli.quickstart_cmd.recommend_engine",
return_value="ollama",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
"openjarvis.cli.quickstart_cmd._check_engine_health",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_model_available",
"openjarvis.cli.quickstart_cmd._check_model_available",
return_value=True,
),
patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
@@ -169,18 +155,15 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
"openjarvis.cli.quickstart_cmd.generate_default_toml",
return_value="[engine]\n",
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
"openjarvis.cli.quickstart_cmd.recommend_engine",
return_value="ollama",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
"openjarvis.cli.quickstart_cmd._check_engine_health",
return_value=False,
),
):
@@ -207,33 +190,27 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch(
"openjarvis.cli.quickstart_cmd"
".generate_default_toml",
return_value="[engine]\ndefault = \"mlx\"\n",
"openjarvis.cli.quickstart_cmd.generate_default_toml",
return_value='[engine]\ndefault = "mlx"\n',
),
patch(
"openjarvis.cli.quickstart_cmd"
".recommend_engine",
"openjarvis.cli.quickstart_cmd.recommend_engine",
return_value="mlx",
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_engine_health",
"openjarvis.cli.quickstart_cmd._check_engine_health",
return_value=False,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._discover_healthy_engines",
"openjarvis.cli.quickstart_cmd._discover_healthy_engines",
return_value=["ollama"],
),
patch(
"openjarvis.cli.quickstart_cmd"
"._check_model_available",
"openjarvis.cli.quickstart_cmd._check_model_available",
return_value=True,
),
patch(
"openjarvis.cli.quickstart_cmd"
"._test_query",
"openjarvis.cli.quickstart_cmd._test_query",
return_value="Hello!",
),
):
+249
View File
@@ -0,0 +1,249 @@
"""Tests for JarvisSystem.wire_channel() — channel → agent routing.
These tests exercise wire_channel() on JarvisSystem directly. The serve.py
entrypoint now delegates all channel-wiring logic there.
"""
from __future__ import annotations
from unittest.mock import MagicMock
from openjarvis.channels._stubs import ChannelMessage
from openjarvis.core.config import JarvisConfig
from openjarvis.core.events import EventBus
from openjarvis.sessions.session import SessionStore
from openjarvis.system import JarvisSystem
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_channel_message(
channel: str = "telegram",
sender: str = "42",
content: str = "hello",
conversation_id: str = "42",
) -> ChannelMessage:
return ChannelMessage(
channel=channel,
sender=sender,
content=content,
message_id="1",
conversation_id=conversation_id,
)
def _make_system(engine=None, agent_name="", tmp_path=None) -> JarvisSystem:
"""Build a minimal JarvisSystem with mock engine for testing wire_channel."""
config = JarvisConfig()
if tmp_path is not None:
config.sessions.db_path = str(tmp_path / "sessions.db")
mock_engine = engine or MagicMock()
return JarvisSystem(
config=config,
bus=EventBus(record_history=False),
engine=mock_engine,
engine_key="mock",
model="test-model",
agent_name=agent_name,
)
def _fire(channel_mock, cm: ChannelMessage) -> None:
"""Invoke all registered on_message handlers as if the channel received cm."""
for handler in channel_mock.on_message.call_args_list:
handler[0][0](cm)
# ---------------------------------------------------------------------------
# Tests via JarvisSystem.wire_channel()
# ---------------------------------------------------------------------------
class TestWireChannelWithAgent:
"""wire_channel routes incoming messages through the agent and replies."""
def test_ask_called_and_reply_sent(self, tmp_path):
system = _make_system(agent_name="simple", tmp_path=tmp_path)
# Patch ask() so we don't need a real engine/agent
system.ask = MagicMock(return_value={"content": "pong"})
mock_channel = MagicMock()
system.wire_channel(mock_channel)
# Simulate an incoming message
cm = _make_channel_message(content="ping")
handler = mock_channel.on_message.call_args[0][0]
handler(cm)
system.ask.assert_called_once()
assert system.ask.call_args[0][0] == "ping"
mock_channel.send.assert_called_once_with(
"telegram",
"pong",
conversation_id="42",
)
def test_session_store_created_lazily(self, tmp_path):
system = _make_system(tmp_path=tmp_path)
assert system.session_store is None
mock_channel = MagicMock()
system.ask = MagicMock(return_value={"content": "ok"})
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message())
assert system.session_store is not None
def test_existing_session_store_reused(self, tmp_path):
system = _make_system(tmp_path=tmp_path)
existing_store = SessionStore(db_path=tmp_path / "sessions.db")
system.session_store = existing_store
system.ask = MagicMock(return_value={"content": "ok"})
mock_channel = MagicMock()
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message())
assert system.session_store is existing_store
class TestWireChannelWithEngine:
"""wire_channel falls back to engine when no agent is set."""
def test_engine_path_used_when_no_agent(self, tmp_path):
system = _make_system(agent_name="", tmp_path=tmp_path)
system.ask = MagicMock(return_value={"content": "raw reply"})
mock_channel = MagicMock()
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message(content="hi"))
mock_channel.send.assert_called_once_with(
"telegram",
"raw reply",
conversation_id="42",
)
class TestWireChannelSessionIsolation:
"""Separate conversation_ids get independent sessions."""
def test_two_chats_isolated(self, tmp_path):
system = _make_system(tmp_path=tmp_path)
replies = {"111": "reply-A", "222": "reply-B"}
system.ask = MagicMock(
side_effect=lambda q, **kw: {"content": replies.get(q, "")}
)
mock_channel = MagicMock()
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message(content="111", conversation_id="111"))
handler(_make_channel_message(content="222", conversation_id="222"))
# Reload sessions — each chat must have only its own messages
s1 = system.session_store.get_or_create("telegram:111")
s2 = system.session_store.get_or_create("telegram:222")
c1 = {m.content for m in s1.messages}
c2 = {m.content for m in s2.messages}
assert "111" in c1 and "222" not in c1
assert "222" in c2 and "111" not in c2
def test_same_chat_accumulates_history(self, tmp_path):
system = _make_system(tmp_path=tmp_path)
system.ask = MagicMock(return_value={"content": "reply"})
mock_channel = MagicMock()
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message(content="first"))
handler(_make_channel_message(content="second"))
session = system.session_store.get_or_create("telegram:42")
contents = [m.content for m in session.messages]
# user + assistant alternating for two turns
assert contents.count("first") == 1
assert contents.count("second") == 1
class TestWireChannelErrorHandling:
"""Handler sends a user-visible error message when ask() raises."""
def test_error_reply_sent(self, tmp_path):
system = _make_system(tmp_path=tmp_path)
system.ask = MagicMock(side_effect=RuntimeError("boom"))
mock_channel = MagicMock()
system.wire_channel(mock_channel)
handler = mock_channel.on_message.call_args[0][0]
handler(_make_channel_message())
mock_channel.send.assert_called_once()
sent_content = mock_channel.send.call_args[0][1]
assert "error" in sent_content.lower()
class TestPerChatSessionIsolation:
"""Direct SessionStore isolation tests (not via wire_channel)."""
def test_two_chats_have_separate_sessions(self, tmp_path):
store = SessionStore(db_path=tmp_path / "sessions.db")
session_a = store.get_or_create(
"telegram:111",
channel="telegram",
channel_user_id="111",
)
store.save_message(session_a.session_id, "user", "msg from A")
session_b = store.get_or_create(
"telegram:222",
channel="telegram",
channel_user_id="222",
)
store.save_message(session_b.session_id, "user", "msg from B")
reloaded_a = store.get_or_create(
"telegram:111",
channel="telegram",
channel_user_id="111",
)
reloaded_b = store.get_or_create(
"telegram:222",
channel="telegram",
channel_user_id="222",
)
contents_a = {m.content for m in reloaded_a.messages}
contents_b = {m.content for m in reloaded_b.messages}
assert "msg from A" in contents_a and "msg from B" not in contents_a
assert "msg from B" in contents_b and "msg from A" not in contents_b
def test_same_chat_accumulates_history(self, tmp_path):
store = SessionStore(db_path=tmp_path / "sessions.db")
session = store.get_or_create(
"telegram:42",
channel="telegram",
channel_user_id="42",
)
store.save_message(session.session_id, "user", "first")
store.save_message(session.session_id, "assistant", "reply")
reloaded = store.get_or_create(
"telegram:42",
channel="telegram",
channel_user_id="42",
)
assert [m.content for m in reloaded.messages] == ["first", "reply"]
+16 -12
View File
@@ -18,16 +18,18 @@ def _populate_db(db_path: Path, n: int = 3) -> None:
"""Create a telemetry DB with *n* records."""
store = TelemetryStore(db_path)
for i in range(n):
store.record(TelemetryRecord(
timestamp=time.time() - (n - i),
model_id=f"model-{i % 2}",
engine="ollama",
prompt_tokens=10 * (i + 1),
completion_tokens=5 * (i + 1),
total_tokens=15 * (i + 1),
latency_seconds=0.5 * (i + 1),
cost_usd=0.001 * (i + 1),
))
store.record(
TelemetryRecord(
timestamp=time.time() - (n - i),
model_id=f"model-{i % 2}",
engine="ollama",
prompt_tokens=10 * (i + 1),
completion_tokens=5 * (i + 1),
total_tokens=15 * (i + 1),
latency_seconds=0.5 * (i + 1),
cost_usd=0.001 * (i + 1),
)
)
store.close()
@@ -37,7 +39,8 @@ def _patch_config(tmp_path: Path):
cfg = mock.MagicMock()
cfg.telemetry.db_path = str(db_path)
return mock.patch(
"openjarvis.cli.telemetry_cmd.load_config", return_value=cfg,
"openjarvis.cli.telemetry_cmd.load_config",
return_value=cfg,
), db_path
@@ -115,7 +118,8 @@ class TestTelemetryExport:
out_file = tmp_path / "export.json"
with patch:
result = CliRunner().invoke(
cli, ["telemetry", "export", "-o", str(out_file)],
cli,
["telemetry", "export", "-o", str(out_file)],
)
assert result.exit_code == 0
assert out_file.exists()
+2 -1
View File
@@ -47,7 +47,8 @@ class TestVaultCmd:
mock.patch("openjarvis.cli.vault_cmd._VAULT_FILE", vault_file),
mock.patch("openjarvis.cli.vault_cmd._VAULT_KEY_FILE", key_file),
mock.patch(
"openjarvis.cli.vault_cmd.DEFAULT_CONFIG_DIR", tmp_path,
"openjarvis.cli.vault_cmd.DEFAULT_CONFIG_DIR",
tmp_path,
),
):
runner = CliRunner()
+7 -2
View File
@@ -54,8 +54,10 @@ def nvidia_gpu() -> GpuInfo:
def nvidia_consumer_gpu() -> GpuInfo:
"""NVIDIA consumer GPU fixture."""
return GpuInfo(
vendor="nvidia", name="NVIDIA GeForce RTX 4090",
vram_gb=24.0, count=1,
vendor="nvidia",
name="NVIDIA GeForce RTX 4090",
vram_gb=24.0,
count=1,
)
@@ -147,6 +149,7 @@ def has_ollama() -> bool:
"""Check if Ollama is running locally."""
try:
import httpx
resp = httpx.get("http://localhost:11434/api/tags", timeout=2.0)
return resp.status_code == 200
except Exception:
@@ -158,6 +161,7 @@ def has_vllm() -> bool:
"""Check if vLLM is running locally."""
try:
import httpx
resp = httpx.get("http://localhost:8000/v1/models", timeout=2.0)
return resp.status_code == 200
except Exception:
@@ -169,6 +173,7 @@ def has_llamacpp() -> bool:
"""Check if llama.cpp server is running locally."""
try:
import httpx
resp = httpx.get("http://localhost:8080/v1/models", timeout=2.0)
return resp.status_code == 200
except Exception:
+10 -12
View File
@@ -200,9 +200,10 @@ class TestAgentConfigNew:
def test_no_temperature_or_max_tokens(self) -> None:
ac = AgentConfig()
assert not hasattr(ac.__class__, "temperature") or isinstance(
getattr(ac.__class__, "temperature", None), property
) is False
assert (
not hasattr(ac.__class__, "temperature")
or isinstance(getattr(ac.__class__, "temperature", None), property) is False
)
class TestNestedEngineConfig:
@@ -288,9 +289,9 @@ class TestNestedLearningConfig:
def test_loads_nested_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\nenabled = true\nupdate_interval = 50\n\n'
"[learning]\nenabled = true\nupdate_interval = 50\n\n"
'[learning.routing]\npolicy = "learned"\n\n'
'[learning.metrics]\nlatency_weight = 0.5\n'
"[learning.metrics]\nlatency_weight = 0.5\n"
)
cfg = load_config(toml_file)
assert cfg.learning.enabled is True
@@ -315,16 +316,14 @@ class TestNestedLearningConfig:
class TestBackwardCompatMigration:
def test_agent_temperature_migrates_to_intelligence(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[agent]\ntemperature = 0.3\nmax_tokens = 512\n'
)
toml_file.write_text("[agent]\ntemperature = 0.3\nmax_tokens = 512\n")
cfg = load_config(toml_file)
assert cfg.intelligence.temperature == 0.3
assert cfg.intelligence.max_tokens == 512
def test_memory_context_injection_migrates_to_agent(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text('[memory]\ncontext_injection = false\n')
toml_file.write_text("[memory]\ncontext_injection = false\n")
cfg = load_config(toml_file)
assert cfg.agent.context_from_memory is False
@@ -400,8 +399,7 @@ class TestSandboxConfig:
def test_loads_from_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[sandbox]\nenabled = true\ntimeout = 600\n'
'runtime = "podman"\n'
'[sandbox]\nenabled = true\ntimeout = 600\nruntime = "podman"\n'
)
cfg = load_config(toml_file)
assert cfg.sandbox.enabled is True
@@ -435,7 +433,7 @@ class TestSchedulerConfig:
def test_loads_from_toml(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[scheduler]\nenabled = true\npoll_interval = 30\n'
"[scheduler]\nenabled = true\npoll_interval = 30\n"
'db_path = "/tmp/sched.db"\n'
)
cfg = load_config(toml_file)
+3 -4
View File
@@ -45,8 +45,7 @@ class TestLearningConfig:
def test_toml_loading_with_learning(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\ndefault_policy = "grpo"\n'
'reward_weights = "latency=0.5"\n'
'[learning]\ndefault_policy = "grpo"\nreward_weights = "latency=0.5"\n'
)
cfg = load_config(toml_file)
assert cfg.learning.routing.policy == "grpo"
@@ -55,9 +54,9 @@ class TestLearningConfig:
def test_toml_loading_nested(self, tmp_path: Path) -> None:
toml_file = tmp_path / "config.toml"
toml_file.write_text(
'[learning]\nenabled = true\n\n'
"[learning]\nenabled = true\n\n"
'[learning.routing]\npolicy = "learned"\n\n'
'[learning.metrics]\nlatency_weight = 0.5\n'
"[learning.metrics]\nlatency_weight = 0.5\n"
)
cfg = load_config(toml_file)
assert cfg.learning.enabled is True
+1
View File
@@ -1,4 +1,5 @@
"""Tests for credential persistence module."""
import os
import pytest
+2
View File
@@ -95,12 +95,14 @@ class TestEventBus:
class TestAgentEventTypes:
def test_agent_tick_events_exist(self):
from openjarvis.core.events import EventType
assert EventType.AGENT_TICK_START
assert EventType.AGENT_TICK_END
assert EventType.AGENT_TICK_ERROR
def test_agent_operational_events_exist(self):
from openjarvis.core.events import EventType
assert EventType.AGENT_BUDGET_EXCEEDED
assert EventType.AGENT_STALL_DETECTED
assert EventType.AGENT_MESSAGE_RECEIVED
+7
View File
@@ -24,12 +24,14 @@ class TestScanResultFromJson:
def test_empty_findings(self):
from openjarvis._rust_bridge import scan_result_from_json
result = scan_result_from_json('{"findings": []}')
assert result.clean
assert result.findings == []
def test_with_findings(self):
from openjarvis._rust_bridge import scan_result_from_json
data = {
"findings": [
{
@@ -54,6 +56,7 @@ class TestInjectionResultFromJson:
def test_clean(self):
from openjarvis._rust_bridge import injection_result_from_json
data = {"is_clean": True, "findings": [], "threat_level": "low"}
result = injection_result_from_json(json.dumps(data))
assert result.is_clean
@@ -61,6 +64,7 @@ class TestInjectionResultFromJson:
def test_with_findings(self):
from openjarvis._rust_bridge import injection_result_from_json
data = {
"is_clean": False,
"findings": [
@@ -86,11 +90,13 @@ class TestRetrievalResultsFromJson:
def test_empty(self):
from openjarvis._rust_bridge import retrieval_results_from_json
results = retrieval_results_from_json("[]")
assert results == []
def test_with_items(self):
from openjarvis._rust_bridge import retrieval_results_from_json
data = [
{
"content": "hello world",
@@ -108,6 +114,7 @@ class TestRetrievalResultsFromJson:
def test_metadata_as_string(self):
from openjarvis._rust_bridge import retrieval_results_from_json
data = [
{
"content": "test",
+78 -47
View File
@@ -324,8 +324,10 @@ class TestCloudAnthropic:
model="claude-opus-4-6",
)
call_kwargs = fake_client.messages.create.call_args
assert call_kwargs.kwargs.get("system") == "You are helpful" or \
call_kwargs[1].get("system") == "You are helpful"
assert (
call_kwargs.kwargs.get("system") == "You are helpful"
or call_kwargs[1].get("system") == "You are helpful"
)
# ---------------------------------------------------------------------------
@@ -339,10 +341,13 @@ class TestCloudGemini:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
fake_genai = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": fake_genai,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": fake_genai,
},
):
if not EngineRegistry.contains("cloud"):
EngineRegistry.register_value("cloud", CloudEngine)
engine = CloudEngine()
@@ -360,11 +365,14 @@ class TestCloudGemini:
fake_config = mock.MagicMock()
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = fake_config
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="gemini-2.5-pro"
)
@@ -383,11 +391,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="gemini-2.5-flash"
)
@@ -403,11 +414,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="gemini-3-pro"
)
@@ -423,11 +437,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="gemini-3-flash"
)
@@ -441,16 +458,16 @@ class TestCloudGemini:
fake_client = mock.MagicMock()
# Build a response with a function_call part
text_part = SimpleNamespace(
text="Let me calculate.", function_call=None
)
text_part = SimpleNamespace(text="Let me calculate.", function_call=None)
fc = SimpleNamespace(name="calculator", args={"expression": "2+2"})
fc_part = SimpleNamespace(text=None, function_call=fc)
content_obj = SimpleNamespace(parts=[text_part, fc_part])
candidate = SimpleNamespace(content=content_obj)
usage = SimpleNamespace(prompt_token_count=10, candidates_token_count=8)
fake_resp = SimpleNamespace(
candidates=[candidate], usage_metadata=usage, text=None,
candidates=[candidate],
usage_metadata=usage,
text=None,
)
fake_client.models.generate_content.return_value = fake_resp
engine._google_client = fake_client
@@ -458,11 +475,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_config = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = fake_config
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="What is 2+2?")],
model="gemini-3-pro",
@@ -497,11 +517,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
result = engine.generate(
[Message(role=Role.USER, content="Hi")], model="gemini-3-pro"
)
@@ -538,11 +561,14 @@ class TestCloudGemini:
fake_types = mock.MagicMock()
fake_types.GenerateContentConfig.return_value = mock.MagicMock()
with mock.patch.dict("sys.modules", {
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
}):
with mock.patch.dict(
"sys.modules",
{
"google": mock.MagicMock(),
"google.genai": mock.MagicMock(),
"google.genai.types": fake_types,
},
):
with pytest.raises(
EngineConnectionError,
match="Google client not available",
@@ -606,8 +632,13 @@ class TestPricingTable:
def test_all_new_models_in_pricing(self) -> None:
expected = [
"gpt-5-mini",
"claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5",
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-3-pro", "gemini-3-flash",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-3-pro",
"gemini-3-flash",
]
for model_id in expected:
assert model_id in PRICING, f"{model_id} missing from PRICING dict"
+1 -3
View File
@@ -54,9 +54,7 @@ class TestDiscoverEngines:
cfg = JarvisConfig()
with mock.patch(
"openjarvis.engine._discovery._make_engine",
side_effect=lambda k, c: _FakeEngine(
healthy=(k == "healthy")
),
side_effect=lambda k, c: _FakeEngine(healthy=(k == "healthy")),
):
result = discover_engines(cfg)
assert len(result) == 1
+83 -50
View File
@@ -40,15 +40,18 @@ def _api_prefix(engine_key: str) -> str:
return ""
return "/v1"
ENGINES_AND_HOSTS = [
(key, host) for key, host, _ in _OPENAI_COMPAT_ENGINES
] + [
ENGINES_AND_HOSTS = [(key, host) for key, host, _ in _OPENAI_COMPAT_ENGINES] + [
("ollama", "http://testhost:11434"),
]
MODELS = [
"gpt-oss:120b", "qwen3:8b", "glm-4.7-flash", "trinity-mini",
"qwen3.5:35b-a3b", "LiquidAI/LFM2.5-1.2B-Instruct-GGUF",
"gpt-oss:120b",
"qwen3:8b",
"glm-4.7-flash",
"trinity-mini",
"qwen3.5:35b-a3b",
"LiquidAI/LFM2.5-1.2B-Instruct-GGUF",
]
_ENGINE_CLASSES = {key: cls for key, _, cls in _OPENAI_COMPAT_ENGINES}
@@ -69,26 +72,34 @@ def _mock_simple_chat(respx_mock, engine_key: str, host: str, model: str):
"""Set up mock for a simple chat response."""
if engine_key == "ollama":
respx_mock.post(f"{host}/api/chat").mock(
return_value=httpx.Response(200, json={
"message": {"role": "assistant", "content": "Hello!"},
"model": model,
"prompt_eval_count": 10,
"eval_count": 5,
"done": True,
})
return_value=httpx.Response(
200,
json={
"message": {"role": "assistant", "content": "Hello!"},
"model": model,
"prompt_eval_count": 10,
"eval_count": 5,
"done": True,
},
)
)
else: # All OpenAI-compatible engines
prefix = _api_prefix(engine_key)
respx_mock.post(f"{host}{prefix}/chat/completions").mock(
return_value=httpx.Response(200, json={
"choices": [
{"message": {"content": "Hello!"}, "finish_reason": "stop"},
],
"usage": {
"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15,
return_value=httpx.Response(
200,
json={
"choices": [
{"message": {"content": "Hello!"}, "finish_reason": "stop"},
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
},
"model": model,
},
"model": model,
})
)
)
@@ -96,39 +107,59 @@ def _mock_tool_call(respx_mock, engine_key: str, host: str, model: str):
"""Set up mock for a tool-call response."""
if engine_key == "ollama":
respx_mock.post(f"{host}/api/chat").mock(
return_value=httpx.Response(200, json={
"message": {
"content": "",
"tool_calls": [{
"function": {"name": "calculator", "arguments": '{"x":1}'},
}],
return_value=httpx.Response(
200,
json={
"message": {
"content": "",
"tool_calls": [
{
"function": {
"name": "calculator",
"arguments": '{"x":1}',
},
}
],
},
"model": model,
"prompt_eval_count": 10,
"eval_count": 8,
"done": True,
},
"model": model,
"prompt_eval_count": 10,
"eval_count": 8,
"done": True,
})
)
)
else: # All OpenAI-compatible engines
prefix = _api_prefix(engine_key)
respx_mock.post(f"{host}{prefix}/chat/completions").mock(
return_value=httpx.Response(200, json={
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "calculator", "arguments": '{"x":1}'},
}],
return_value=httpx.Response(
200,
json={
"choices": [
{
"message": {
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"x":1}',
},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 8,
"total_tokens": 18,
},
"finish_reason": "tool_calls",
}],
"usage": {
"prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18,
"model": model,
},
"model": model,
})
)
)
@@ -176,9 +207,7 @@ class TestEngineScenarios:
engine = _create_engine(engine_key, host)
_mock_error(respx_mock, engine_key, host)
with pytest.raises(EngineConnectionError):
engine.generate(
[Message(role=Role.USER, content="Hi")], model="qwen3:8b"
)
engine.generate([Message(role=Role.USER, content="Hi")], model="qwen3:8b")
# ---------------------------------------------------------------------------
@@ -190,7 +219,11 @@ class TestEngineScenarios:
@pytest.mark.parametrize("model_id", MODELS)
class TestEngineModelMatrix:
def test_generate_with_model(
self, respx_mock, engine_key: str, host: str, model_id: str,
self,
respx_mock,
engine_key: str,
host: str,
model_id: str,
) -> None:
engine = _create_engine(engine_key, host)
_mock_simple_chat(respx_mock, engine_key, host, model_id)
+4 -1
View File
@@ -85,7 +85,9 @@ class TestLlamaCppGenerate:
return_value=httpx.Response(
200,
json=_openai_response(
content="", model=model_id, tool_calls=tool_calls,
content="",
model=model_id,
tool_calls=tool_calls,
),
)
)
@@ -143,6 +145,7 @@ class TestLlamaCppGenerate:
return tokens
import asyncio
tokens = asyncio.run(collect())
assert tokens == ["Hi", " there"]
+1 -3
View File
@@ -51,9 +51,7 @@ class TestMLXGenerate:
side_effect=httpx.ConnectError("refused")
)
with pytest.raises(EngineConnectionError):
engine.generate(
[Message(role=Role.USER, content="Hi")], model="m"
)
engine.generate([Message(role=Role.USER, content="Hi")], model="m")
class TestMLXHealth:
+9 -11
View File
@@ -81,7 +81,9 @@ class TestOllamaGenerate:
return_value=httpx.Response(
200,
json=_ollama_response(
content="", model=model_id, tool_calls=tool_calls,
content="",
model=model_id,
tool_calls=tool_calls,
),
)
)
@@ -104,7 +106,8 @@ class TestOllamaGenerate:
return_value=httpx.Response(
200,
json=_ollama_response(
content="", model=model_id,
content="",
model=model_id,
tool_calls=tool_calls,
),
)
@@ -136,6 +139,7 @@ class TestOllamaGenerate:
return tokens
import asyncio
tokens = asyncio.run(collect())
assert "Hello" in tokens
@@ -222,9 +226,7 @@ class TestOllamaErrors:
def capture(request):
captured["body"] = json.loads(request.content)
return httpx.Response(
200, json=_ollama_response(content="ok")
)
return httpx.Response(200, json=_ollama_response(content="ok"))
respx_mock.post(f"{OLLAMA_HOST}/api/chat").mock(side_effect=capture)
engine.generate(
@@ -241,12 +243,8 @@ class TestOllamaErrors:
def capture(request):
captured["body"] = json.loads(request.content)
return httpx.Response(
200, json=_ollama_response(content="ok")
)
return httpx.Response(200, json=_ollama_response(content="ok"))
respx_mock.post(f"{OLLAMA_HOST}/api/chat").mock(side_effect=capture)
engine.generate(
[Message(role=Role.USER, content="Hello")], model="qwen3:8b"
)
engine.generate([Message(role=Role.USER, content="Hello")], model="qwen3:8b")
assert "tools" not in captured["body"]
+3 -4
View File
@@ -47,12 +47,11 @@ class TestOpenAICompatGenerate:
assert result["usage"]["total_tokens"] == 9
def test_empty_choices_returns_graceful_fallback(
self, engine: VLLMEngine,
self,
engine: VLLMEngine,
) -> None:
with respx.mock:
respx.post(
"http://testhost:8000/v1/chat/completions"
).mock(
respx.post("http://testhost:8000/v1/chat/completions").mock(
return_value=httpx.Response(
200,
json={
+135 -96
View File
@@ -24,17 +24,20 @@ class TestOpenAICompatToolCalls:
def test_no_tool_calls(self, respx_mock):
"""When no tool_calls in response, result has no tool_calls key."""
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
return_value=httpx.Response(200, json={
"choices": [
{"message": {"content": "Hi"}, "finish_reason": "stop"},
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 2,
"total_tokens": 7,
return_value=httpx.Response(
200,
json={
"choices": [
{"message": {"content": "Hi"}, "finish_reason": "stop"},
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 2,
"total_tokens": 7,
},
"model": "test",
},
"model": "test",
})
)
)
engine = _TestEngine()
result = engine.generate(
@@ -47,28 +50,35 @@ class TestOpenAICompatToolCalls:
def test_with_tool_calls(self, respx_mock):
"""Extract tool_calls from OpenAI-format response."""
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
return_value=httpx.Response(200, json={
"choices": [{
"message": {
"content": None,
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"expression":"2+2"}',
return_value=httpx.Response(
200,
json={
"choices": [
{
"message": {
"content": None,
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"expression":"2+2"}',
},
}
],
},
}],
"finish_reason": "tool_calls",
}
],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 10,
"total_tokens": 15,
},
"finish_reason": "tool_calls",
}],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 10,
"total_tokens": 15,
"model": "test",
},
"model": "test",
})
)
)
engine = _TestEngine()
result = engine.generate(
@@ -89,11 +99,16 @@ class TestOpenAICompatToolCalls:
def capture(request):
captured["body"] = json.loads(request.content)
return httpx.Response(200, json={
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
"usage": {},
"model": "test",
})
return httpx.Response(
200,
json={
"choices": [
{"message": {"content": "ok"}, "finish_reason": "stop"}
],
"usage": {},
"model": "test",
},
)
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
side_effect=capture,
@@ -108,26 +123,33 @@ class TestOpenAICompatToolCalls:
def test_multiple_tool_calls(self, respx_mock):
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
return_value=httpx.Response(200, json={
"choices": [{
"message": {
"content": "",
"tool_calls": [
{
"id": "c1", "type": "function",
"function": {"name": "a", "arguments": "{}"},
return_value=httpx.Response(
200,
json={
"choices": [
{
"message": {
"content": "",
"tool_calls": [
{
"id": "c1",
"type": "function",
"function": {"name": "a", "arguments": "{}"},
},
{
"id": "c2",
"type": "function",
"function": {"name": "b", "arguments": "{}"},
},
],
},
{
"id": "c2", "type": "function",
"function": {"name": "b", "arguments": "{}"},
},
],
},
"finish_reason": "tool_calls",
}],
"usage": {},
"model": "test",
})
"finish_reason": "tool_calls",
}
],
"usage": {},
"model": "test",
},
)
)
engine = _TestEngine()
result = engine.generate(
@@ -145,12 +167,15 @@ class TestOpenAICompatToolCalls:
class TestOllamaToolCalls:
def test_no_tool_calls(self, respx_mock):
respx_mock.post("http://localhost:11434/api/chat").mock(
return_value=httpx.Response(200, json={
"message": {"content": "Hi"},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 2,
})
return_value=httpx.Response(
200,
json={
"message": {"content": "Hi"},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 2,
},
)
)
engine = OllamaEngine()
result = engine.generate(
@@ -161,20 +186,25 @@ class TestOllamaToolCalls:
def test_with_tool_calls(self, respx_mock):
respx_mock.post("http://localhost:11434/api/chat").mock(
return_value=httpx.Response(200, json={
"message": {
"content": "",
"tool_calls": [{
"function": {
"name": "calculator",
"arguments": '{"expression":"3*3"}',
},
}],
return_value=httpx.Response(
200,
json={
"message": {
"content": "",
"tool_calls": [
{
"function": {
"name": "calculator",
"arguments": '{"expression":"3*3"}',
},
}
],
},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 3,
},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 3,
})
)
)
engine = OllamaEngine()
result = engine.generate(
@@ -189,10 +219,13 @@ class TestOllamaToolCalls:
def capture(request):
captured["body"] = json.loads(request.content)
return httpx.Response(200, json={
"message": {"content": "ok"},
"model": "test",
})
return httpx.Response(
200,
json={
"message": {"content": "ok"},
"model": "test",
},
)
respx_mock.post("http://localhost:11434/api/chat").mock(side_effect=capture)
engine = OllamaEngine()
@@ -205,23 +238,26 @@ class TestOllamaToolCalls:
def test_dict_arguments_serialized_to_json(self, respx_mock):
"""Ollama returns arguments as dict — engine must serialize."""
respx_mock.post(
"http://localhost:11434/api/chat"
).mock(
return_value=httpx.Response(200, json={
"message": {
"content": "",
"tool_calls": [{
"function": {
"name": "calculator",
"arguments": {"expression": "3*3"},
},
}],
respx_mock.post("http://localhost:11434/api/chat").mock(
return_value=httpx.Response(
200,
json={
"message": {
"content": "",
"tool_calls": [
{
"function": {
"name": "calculator",
"arguments": {"expression": "3*3"},
},
}
],
},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 3,
},
"model": "test",
"prompt_eval_count": 5,
"eval_count": 3,
})
)
)
engine = OllamaEngine()
result = engine.generate(
@@ -240,10 +276,13 @@ class TestOllamaToolCalls:
def capture(request):
captured["body"] = json.loads(request.content)
return httpx.Response(200, json={
"message": {"content": "ok"},
"model": "test",
})
return httpx.Response(
200,
json={
"message": {"content": "ok"},
"model": "test",
},
)
respx_mock.post("http://localhost:11434/api/chat").mock(side_effect=capture)
engine = OllamaEngine()
+5 -15
View File
@@ -180,9 +180,7 @@ class TestAnthropicStructuredOutput:
json_tool = [t for t in call_kwargs["tools"] if t["name"] == "json_output"][0]
assert json_tool["input_schema"] == schema
def test_appends_to_existing_tools(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_appends_to_existing_tools(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine, fake_client = self._make_engine(monkeypatch)
rf = ResponseFormat()
existing_tools = [
@@ -228,12 +226,8 @@ class TestGoogleStructuredOutput:
text='{"answer": 42}',
function_call=None,
)
fake_candidate = SimpleNamespace(
content=SimpleNamespace(parts=[fake_part])
)
fake_um = SimpleNamespace(
prompt_token_count=10, candidates_token_count=5
)
fake_candidate = SimpleNamespace(content=SimpleNamespace(parts=[fake_part]))
fake_um = SimpleNamespace(prompt_token_count=10, candidates_token_count=5)
fake_resp = SimpleNamespace(
candidates=[fake_candidate],
usage_metadata=fake_um,
@@ -242,9 +236,7 @@ class TestGoogleStructuredOutput:
fake_client.models.generate_content.return_value = fake_resp
return engine, fake_client
def test_json_mode_sets_mime_type(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_json_mode_sets_mime_type(self, monkeypatch: pytest.MonkeyPatch) -> None:
engine, fake_client = self._make_engine(monkeypatch)
rf = ResponseFormat()
@@ -361,9 +353,7 @@ class TestOllamaStructuredOutput:
sent_payload = json.loads(route.calls[0].request.content)
assert sent_payload["format"] == "json"
def test_no_format_without_response_format(
self, engine: OllamaEngine
) -> None:
def test_no_format_without_response_format(self, engine: OllamaEngine) -> None:
with respx.mock:
route = respx.post("http://testhost:11434/api/chat").mock(
return_value=httpx.Response(
+5 -4
View File
@@ -84,7 +84,9 @@ class TestVLLMGenerate:
return_value=httpx.Response(
200,
json=_openai_response(
content="", model=model_id, tool_calls=tool_calls,
content="",
model=model_id,
tool_calls=tool_calls,
),
)
)
@@ -112,9 +114,7 @@ class TestVLLMGenerate:
200, json=_openai_response(content="Fallback reply", model=model_id)
)
respx_mock.post(f"{VLLM_HOST}/v1/chat/completions").mock(
side_effect=handler
)
respx_mock.post(f"{VLLM_HOST}/v1/chat/completions").mock(side_effect=handler)
result = engine.generate(
[Message(role=Role.USER, content="Hello")],
model=model_id,
@@ -144,6 +144,7 @@ class TestVLLMGenerate:
return tokens
import asyncio
tokens = asyncio.run(collect())
assert tokens == ["Hello", " world"]
@@ -11,9 +11,7 @@ def _make_record(exact_facts=None, semantic_facts=None):
if exact_facts:
all_facts += [{"fact": f, "type": "exact"} for f in exact_facts]
if semantic_facts:
all_facts += [
{"fact": f, "type": "semantic"} for f in semantic_facts
]
all_facts += [{"fact": f, "type": "semantic"} for f in semantic_facts]
return EvalRecord(
record_id="test-ba-1",
problem="Research this topic.",
@@ -76,9 +74,7 @@ def test_sources_with_url():
record = _make_record(exact_facts=["5432"])
scorer = BrowserAssistantScorer()
answer = (
"Port 5432. See https://www.postgresql.org/docs/"
)
answer = "Port 5432. See https://www.postgresql.org/docs/"
is_correct, meta = scorer.score(record, answer)
assert meta["sources_cited"] is True
+1 -4
View File
@@ -64,10 +64,7 @@ def test_checklist_scorer_all_pass():
def test_checklist_scorer_partial():
response = (
"1. yes — Redis is mentioned\n"
"2. no — Port number not found\n"
)
response = "1. yes — Redis is mentioned\n2. no — Port number not found\n"
backend = FakeJudgeBackend(response)
scorer = ChecklistScorer(backend, "test-model")
score, details = scorer.score_checklist(
+4 -2
View File
@@ -28,7 +28,8 @@ def test_all_tests_fixed():
"def test_neg(): assert add(-1, 1) == 0\n"
)
record = _make_record(
buggy, tests,
buggy,
tests,
["test_basic", "test_neg"],
["test_zero"],
)
@@ -49,7 +50,8 @@ def test_partial_fix():
"def test_zero(): assert div(0, 1) == 0.0\n"
)
record = _make_record(
buggy, tests,
buggy,
tests,
["test_zero_div"], # not actually testable here
["test_normal", "test_zero"],
)
+4 -10
View File
@@ -23,10 +23,8 @@ def _make_record(required_facts):
def test_all_facts_with_citations():
facts = [
{"fact": "reclaims storage from dead tuples",
"source_doc_index": 0},
{"fact": "must run periodically",
"source_doc_index": 0},
{"fact": "reclaims storage from dead tuples", "source_doc_index": 0},
{"fact": "must run periodically", "source_doc_index": 0},
]
record = _make_record(facts)
scorer = DocQAScorer()
@@ -45,8 +43,7 @@ def test_all_facts_with_citations():
def test_facts_without_citations():
facts = [
{"fact": "reclaims storage from dead tuples",
"source_doc_index": 0},
{"fact": "reclaims storage from dead tuples", "source_doc_index": 0},
]
record = _make_record(facts)
scorer = DocQAScorer()
@@ -70,10 +67,7 @@ def test_partial_facts():
record = _make_record(facts)
scorer = DocQAScorer()
answer = (
"VACUUM reclaims storage [Doc 1]. "
"It also prevents table bloat [Doc 1]."
)
answer = "VACUUM reclaims storage [Doc 1]. It also prevents table bloat [Doc 1]."
is_correct, meta = scorer.score(record, answer)
assert meta["facts_found"] == 2
assert 0.6 <= meta["fact_score"] <= 0.7
+4 -7
View File
@@ -62,8 +62,7 @@ def test_partial_detection():
scorer = SecurityScannerScorer()
answer = (
"Found SQL injection in app.py. "
"Severity: critical. Use parameterized queries."
"Found SQL injection in app.py. Severity: critical. Use parameterized queries."
)
is_correct, meta = scorer.score(record, answer)
assert meta["vulns_found"] == 1
@@ -107,10 +106,7 @@ def test_vuln_type_aliases():
record = _make_record(vulns)
scorer = SecurityScannerScorer()
answer = (
"app.py has a cross-site scripting "
"vulnerability. Severity: high."
)
answer = "app.py has a cross-site scripting vulnerability. Severity: high."
is_correct, meta = scorer.score(record, answer)
assert meta["vulns_found"] == 1
assert meta["detection_rate"] == 1.0
@@ -120,7 +116,8 @@ def test_no_vulnerabilities():
record = _make_record([])
scorer = SecurityScannerScorer()
is_correct, meta = scorer.score(
record, "Everything looks clean.",
record,
"Everything looks clean.",
)
assert is_correct is None
assert meta["reason"] == "no_vulnerabilities_in_manifest"
+2 -6
View File
@@ -121,9 +121,7 @@ class TestAgenticRunner:
def test_artifacts_saved(self, tmp_path):
records = [MockRecord(record_id="r1", problem="test")]
dataset = MockDataset(records)
runner = AgenticRunner(
agent=MockAgent(), dataset=dataset, run_dir=tmp_path
)
runner = AgenticRunner(agent=MockAgent(), dataset=dataset, run_dir=tmp_path)
self._run_async(runner.run())
arts = tmp_path / "artifacts"
@@ -137,9 +135,7 @@ class TestAgenticRunner:
"""Verify timeout is stored and runner accepts the parameter."""
records = [MockRecord(record_id="r1", problem="test")]
dataset = MockDataset(records)
runner = AgenticRunner(
agent=MockAgent(), dataset=dataset, query_timeout=30.0
)
runner = AgenticRunner(agent=MockAgent(), dataset=dataset, query_timeout=30.0)
assert runner._query_timeout == 30.0
+10 -8
View File
@@ -91,6 +91,7 @@ class TestAMABenchDataset:
def test_question_types_mapped(self) -> None:
from openjarvis.evals.datasets.ama_bench import _QUESTION_TYPE_TO_SUBJECT
assert _QUESTION_TYPE_TO_SUBJECT["A"] == "recall"
assert _QUESTION_TYPE_TO_SUBJECT["B"] == "causal_inference"
assert _QUESTION_TYPE_TO_SUBJECT["C"] == "state_updating"
@@ -111,8 +112,10 @@ class TestAMABenchScorer:
def test_empty_response(self) -> None:
s = AMABenchScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="test-1", problem="question",
reference="answer", category="agentic",
record_id="test-1",
problem="question",
reference="answer",
category="agentic",
)
is_correct, meta = s.score(record, "")
assert is_correct is False
@@ -175,14 +178,10 @@ class TestJudgeParsing:
assert _parse_judge_label("yes.") == "yes"
def test_with_thinking_tags(self) -> None:
assert _parse_judge_label(
"<think>Let me check...</think>yes"
) == "yes"
assert _parse_judge_label("<think>Let me check...</think>yes") == "yes"
def test_with_thinking_tags_no(self) -> None:
assert _parse_judge_label(
"<think>The answer doesn't match</think>\nno"
) == "no"
assert _parse_judge_label("<think>The answer doesn't match</think>\nno") == "no"
def test_multiline(self) -> None:
assert _parse_judge_label(" \n yes\n") == "yes"
@@ -216,14 +215,17 @@ class TestTokenF1:
class TestAMABenchCLI:
def test_in_benchmarks_dict(self) -> None:
from openjarvis.evals.cli import BENCHMARKS
assert "ama-bench" in BENCHMARKS
def test_build_dataset(self) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset("ama-bench")
assert ds.dataset_id == "ama-bench"
def test_build_scorer(self) -> None:
from openjarvis.evals.cli import _build_scorer
s = _build_scorer("ama-bench", _mock_backend(), "test-model")
assert s.scorer_id == "ama-bench"
+68 -9
View File
@@ -24,84 +24,98 @@ class TestDatasetInstantiation:
def test_supergpqa(self) -> None:
from openjarvis.evals.datasets.supergpqa import SuperGPQADataset
ds = SuperGPQADataset()
assert ds.dataset_id == "supergpqa"
assert ds.dataset_name == "SuperGPQA"
def test_gpqa(self) -> None:
from openjarvis.evals.datasets.gpqa import GPQADataset
ds = GPQADataset()
assert ds.dataset_id == "gpqa"
assert ds.dataset_name == "GPQA"
def test_mmlu_pro(self) -> None:
from openjarvis.evals.datasets.mmlu_pro import MMLUProDataset
ds = MMLUProDataset()
assert ds.dataset_id == "mmlu-pro"
assert ds.dataset_name == "MMLU-Pro"
def test_math500(self) -> None:
from openjarvis.evals.datasets.math500 import MATH500Dataset
ds = MATH500Dataset()
assert ds.dataset_id == "math500"
assert ds.dataset_name == "MATH-500"
def test_natural_reasoning(self) -> None:
from openjarvis.evals.datasets.natural_reasoning import NaturalReasoningDataset
ds = NaturalReasoningDataset()
assert ds.dataset_id == "natural-reasoning"
assert ds.dataset_name == "Natural Reasoning"
def test_hle(self) -> None:
from openjarvis.evals.datasets.hle import HLEDataset
ds = HLEDataset()
assert ds.dataset_id == "hle"
assert ds.dataset_name == "HLE"
def test_simpleqa(self) -> None:
from openjarvis.evals.datasets.simpleqa import SimpleQADataset
ds = SimpleQADataset()
assert ds.dataset_id == "simpleqa"
assert ds.dataset_name == "SimpleQA"
def test_wildchat(self) -> None:
from openjarvis.evals.datasets.wildchat import WildChatDataset
ds = WildChatDataset()
assert ds.dataset_id == "wildchat"
assert ds.dataset_name == "WildChat"
def test_ipw(self) -> None:
from openjarvis.evals.datasets.ipw_mixed import IPWDataset
ds = IPWDataset()
assert ds.dataset_id == "ipw"
assert ds.dataset_name == "IPW"
def test_gaia(self) -> None:
from openjarvis.evals.datasets.gaia import GAIADataset
ds = GAIADataset()
assert ds.dataset_id == "gaia"
assert ds.dataset_name == "GAIA"
def test_frames(self) -> None:
from openjarvis.evals.datasets.frames import FRAMESDataset
ds = FRAMESDataset()
assert ds.dataset_id == "frames"
assert ds.dataset_name == "FRAMES"
def test_swebench(self) -> None:
from openjarvis.evals.datasets.swebench import SWEBenchDataset
ds = SWEBenchDataset()
assert ds.dataset_id == "swebench"
assert ds.dataset_name == "SWE-bench"
def test_swefficiency(self) -> None:
from openjarvis.evals.datasets.swefficiency import SWEfficiencyDataset
ds = SWEfficiencyDataset()
assert ds.dataset_id == "swefficiency"
assert ds.dataset_name == "SWEfficiency"
def test_terminalbench(self) -> None:
from openjarvis.evals.datasets.terminalbench import TerminalBenchDataset
ds = TerminalBenchDataset()
assert ds.dataset_id == "terminalbench"
assert ds.dataset_name == "TerminalBench"
@@ -110,6 +124,7 @@ class TestDatasetInstantiation:
from openjarvis.evals.datasets.terminalbench_native import (
TerminalBenchNativeDataset,
)
ds = TerminalBenchNativeDataset()
assert ds.dataset_id == "terminalbench-native"
assert ds.dataset_name == "TerminalBench Native"
@@ -132,56 +147,67 @@ class TestScorerInstantiation:
def test_supergpqa_scorer(self) -> None:
from openjarvis.evals.scorers.supergpqa_mcq import SuperGPQAScorer
s = SuperGPQAScorer(_mock_backend(), "test-model")
assert s.scorer_id == "supergpqa"
def test_gpqa_scorer(self) -> None:
from openjarvis.evals.scorers.gpqa_mcq import GPQAScorer
s = GPQAScorer(_mock_backend(), "test-model")
assert s.scorer_id == "gpqa"
def test_mmlu_pro_scorer(self) -> None:
from openjarvis.evals.scorers.mmlu_pro_mcq import MMLUProScorer
s = MMLUProScorer(_mock_backend(), "test-model")
assert s.scorer_id == "mmlu-pro"
def test_reasoning_judge_scorer(self) -> None:
from openjarvis.evals.scorers.reasoning_judge import ReasoningJudgeScorer
s = ReasoningJudgeScorer(_mock_backend(), "test-model")
assert s.scorer_id == "reasoning_judge"
def test_hle_scorer(self) -> None:
from openjarvis.evals.scorers.hle_judge import HLEScorer
s = HLEScorer(_mock_backend(), "test-model")
assert s.scorer_id == "hle"
def test_simpleqa_scorer(self) -> None:
from openjarvis.evals.scorers.simpleqa_judge import SimpleQAScorer
s = SimpleQAScorer(_mock_backend(), "test-model")
assert s.scorer_id == "simpleqa"
def test_wildchat_scorer(self) -> None:
from openjarvis.evals.scorers.wildchat_judge import WildChatScorer
s = WildChatScorer(_mock_backend(), "test-model")
assert s.scorer_id == "wildchat"
def test_ipw_mixed_scorer(self) -> None:
from openjarvis.evals.scorers.ipw_mixed import IPWMixedScorer
s = IPWMixedScorer(_mock_backend(), "test-model")
assert s.scorer_id == "ipw"
def test_gaia_scorer(self) -> None:
from openjarvis.evals.scorers.gaia_exact import GAIAScorer
s = GAIAScorer(_mock_backend(), "test-model")
assert s.scorer_id == "gaia"
def test_frames_scorer(self) -> None:
from openjarvis.evals.scorers.frames_judge import FRAMESScorer
s = FRAMESScorer(_mock_backend(), "test-model")
assert s.scorer_id == "frames"
def test_swebench_scorer(self) -> None:
from openjarvis.evals.scorers.swebench_structural import SWEBenchScorer
s = SWEBenchScorer(_mock_backend(), "test-model")
assert s.scorer_id == "swebench"
@@ -189,11 +215,13 @@ class TestScorerInstantiation:
from openjarvis.evals.scorers.swefficiency_structural import (
SWEfficiencyScorer,
)
s = SWEfficiencyScorer(_mock_backend(), "test-model")
assert s.scorer_id == "swefficiency"
def test_terminalbench_scorer(self) -> None:
from openjarvis.evals.scorers.terminalbench_judge import TerminalBenchScorer
s = TerminalBenchScorer(_mock_backend(), "test-model")
assert s.scorer_id == "terminalbench"
@@ -201,6 +229,7 @@ class TestScorerInstantiation:
from openjarvis.evals.scorers.terminalbench_native_structural import (
TerminalBenchNativeScorer,
)
s = TerminalBenchNativeScorer(_mock_backend(), "test-model")
assert s.scorer_id == "terminalbench-native"
@@ -211,9 +240,21 @@ class TestScorerInstantiation:
ALL_BENCHMARKS = [
"supergpqa", "gpqa", "mmlu-pro", "math500", "natural-reasoning",
"hle", "simpleqa", "wildchat", "ipw", "gaia", "frames",
"swebench", "swefficiency", "terminalbench", "terminalbench-native",
"supergpqa",
"gpqa",
"mmlu-pro",
"math500",
"natural-reasoning",
"hle",
"simpleqa",
"wildchat",
"ipw",
"gaia",
"frames",
"swebench",
"swefficiency",
"terminalbench",
"terminalbench-native",
]
@@ -223,6 +264,7 @@ class TestCLIFactories:
@pytest.mark.parametrize("benchmark", ALL_BENCHMARKS)
def test_build_dataset(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset(benchmark)
assert ds is not None
assert hasattr(ds, "load")
@@ -232,6 +274,7 @@ class TestCLIFactories:
@pytest.mark.parametrize("benchmark", ALL_BENCHMARKS)
def test_build_scorer(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_scorer
scorer = _build_scorer(benchmark, _mock_backend(), "test-model")
assert scorer is not None
assert hasattr(scorer, "score")
@@ -240,6 +283,7 @@ class TestCLIFactories:
import click
from openjarvis.evals.cli import _build_dataset
with pytest.raises(click.ClickException, match="Unknown benchmark"):
_build_dataset("nonexistent")
@@ -247,6 +291,7 @@ class TestCLIFactories:
import click
from openjarvis.evals.cli import _build_scorer
with pytest.raises(click.ClickException, match="Unknown benchmark"):
_build_scorer("nonexistent", _mock_backend(), "test-model")
@@ -261,11 +306,13 @@ class TestConfigBenchmarks:
def test_all_benchmarks_known(self) -> None:
from openjarvis.evals.core.config import KNOWN_BENCHMARKS
for b in ALL_BENCHMARKS:
assert b in KNOWN_BENCHMARKS, f"{b} missing from KNOWN_BENCHMARKS"
def test_benchmarks_count(self) -> None:
from openjarvis.evals.core.config import KNOWN_BENCHMARKS
assert len(KNOWN_BENCHMARKS) == 25
@@ -280,9 +327,12 @@ class TestStructuralScorers:
def test_swebench_empty_response(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.swebench_structural import SWEBenchScorer
scorer = SWEBenchScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="swe-1", problem="Fix bug", reference="patch",
record_id="swe-1",
problem="Fix bug",
reference="patch",
category="agentic",
)
is_correct, meta = scorer.score(record, "")
@@ -292,9 +342,12 @@ class TestStructuralScorers:
def test_swebench_with_diff(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.swebench_structural import SWEBenchScorer
scorer = SWEBenchScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="swe-2", problem="Fix bug", reference="patch",
record_id="swe-2",
problem="Fix bug",
reference="patch",
category="agentic",
)
answer = "--- a/file.py\n+++ b/file.py\n@@ -1 +1 @@\n-old\n+new"
@@ -308,10 +361,13 @@ class TestStructuralScorers:
from openjarvis.evals.scorers.terminalbench_native_structural import (
TerminalBenchNativeScorer,
)
scorer = TerminalBenchNativeScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="tb-1", problem="Run command",
reference="", category="agentic",
record_id="tb-1",
problem="Run command",
reference="",
category="agentic",
)
is_correct, meta = scorer.score(record, "some output")
assert is_correct is None
@@ -322,10 +378,13 @@ class TestStructuralScorers:
from openjarvis.evals.scorers.terminalbench_native_structural import (
TerminalBenchNativeScorer,
)
scorer = TerminalBenchNativeScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="tb-2", problem="Run command",
reference="", category="agentic",
record_id="tb-2",
problem="Run command",
reference="",
category="agentic",
metadata={"is_resolved": True},
)
is_correct, meta = scorer.score(record, "output")
+7 -2
View File
@@ -63,8 +63,10 @@ class TestDeepPlanningScorer:
def test_empty_response(self) -> None:
s = DeepPlanningScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="dp-3", problem="task",
reference="answer", category="agentic",
record_id="dp-3",
problem="task",
reference="answer",
category="agentic",
)
is_correct, meta = s.score(record, "")
assert is_correct is False
@@ -74,14 +76,17 @@ class TestDeepPlanningScorer:
class TestDeepPlanningCLI:
def test_in_benchmarks(self) -> None:
from openjarvis.evals.cli import BENCHMARKS
assert "deepplanning" in BENCHMARKS
def test_build_dataset(self) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset("deepplanning")
assert ds.dataset_id == "deepplanning"
def test_build_scorer(self) -> None:
from openjarvis.evals.cli import _build_scorer
s = _build_scorer("deepplanning", _mock_backend(), "test-model")
assert s.scorer_id == "deepplanning"
+10 -3
View File
@@ -45,8 +45,14 @@ def _make_summary(**overrides) -> RunSummary:
def _make_metric_stats(**kw) -> MetricStats:
defaults = dict(
mean=1.0, median=0.9, min=0.1, max=2.5,
std=0.3, p90=2.0, p95=2.2, p99=2.4,
mean=1.0,
median=0.9,
min=0.1,
max=2.5,
std=0.3,
p90=2.0,
p95=2.2,
p99=2.4,
)
defaults.update(kw)
return MetricStats(**defaults)
@@ -189,7 +195,8 @@ class TestPrintCompletion:
summary = _make_summary()
console, buf = _make_console()
print_completion(
console, summary,
console,
summary,
output_path=Path("results/test.jsonl"),
traces_dir=Path("results/traces/supergpqa_qwen3-8b"),
)
+5 -1
View File
@@ -37,13 +37,17 @@ class TestDatasetProviderEpisodes:
class TestRunConfigEpisodeMode:
def test_episode_mode_field(self) -> None:
from openjarvis.evals.core.types import RunConfig
cfg = RunConfig(
benchmark="test", backend="test", model="test",
benchmark="test",
backend="test",
model="test",
episode_mode=True,
)
assert cfg.episode_mode is True
def test_episode_mode_default_false(self) -> None:
from openjarvis.evals.core.types import RunConfig
cfg = RunConfig(benchmark="test", backend="test", model="test")
assert cfg.episode_mode is False
+74 -57
View File
@@ -17,25 +17,27 @@ from openjarvis.evals.core.trace import QueryTrace, TurnTrace
def _make_traces(n=3):
traces = []
for i in range(n):
traces.append(QueryTrace(
query_id=f"q{i:04d}",
workload_type="test",
query_text=f"Question {i}",
response_text=f"Answer {i}",
turns=[
TurnTrace(
turn_index=0,
input_tokens=100 + i * 10,
output_tokens=50 + i * 5,
wall_clock_s=1.0 + i * 0.5,
gpu_energy_joules=5.0 + i,
cost_usd=0.01,
),
],
total_wall_clock_s=1.0 + i * 0.5,
completed=True,
is_resolved=i % 2 == 0,
))
traces.append(
QueryTrace(
query_id=f"q{i:04d}",
workload_type="test",
query_text=f"Question {i}",
response_text=f"Answer {i}",
turns=[
TurnTrace(
turn_index=0,
input_tokens=100 + i * 10,
output_tokens=50 + i * 5,
wall_clock_s=1.0 + i * 0.5,
gpu_energy_joules=5.0 + i,
cost_usd=0.01,
),
],
total_wall_clock_s=1.0 + i * 0.5,
completed=True,
is_resolved=i % 2 == 0,
)
)
return traces
@@ -89,11 +91,20 @@ class TestExportSummaryJson:
summary = json.loads(path.read_text())
stats = summary["statistics"]
expected_stat_keys = {
"wall_clock_s", "gpu_energy_joules", "cpu_energy_joules",
"gpu_power_watts", "cpu_power_watts",
"input_tokens", "output_tokens", "total_tokens",
"throughput_tokens_per_sec", "energy_per_token_joules",
"cost_usd", "turns", "tool_calls", "mbu_avg_pct",
"wall_clock_s",
"gpu_energy_joules",
"cpu_energy_joules",
"gpu_power_watts",
"cpu_power_watts",
"input_tokens",
"output_tokens",
"total_tokens",
"throughput_tokens_per_sec",
"energy_per_token_joules",
"cost_usd",
"turns",
"tool_calls",
"mbu_avg_pct",
}
assert set(stats.keys()) == expected_stat_keys
@@ -152,8 +163,10 @@ class TestComputeEfficiency:
def test_no_scored_traces(self):
traces = [
QueryTrace(
query_id="q0", workload_type="test",
completed=True, is_resolved=None,
query_id="q0",
workload_type="test",
completed=True,
is_resolved=None,
),
]
result = _compute_efficiency(traces, 5.0, 1.0)
@@ -169,8 +182,10 @@ class TestComputeEfficiency:
def test_with_gpu_power(self):
traces = [
QueryTrace(
query_id="q0", workload_type="test",
completed=True, is_resolved=True,
query_id="q0",
workload_type="test",
completed=True,
is_resolved=True,
query_gpu_power_avg_watts=100.0,
),
]
@@ -351,35 +366,37 @@ class TestActionEnergyBreakdown:
def test_action_energy_summary_in_export(self, tmp_path):
traces = []
for i in range(2):
traces.append(QueryTrace(
query_id=f"q{i:04d}",
workload_type="test",
turns=[
TurnTrace(
turn_index=0,
input_tokens=100,
output_tokens=50,
wall_clock_s=2.0,
gpu_energy_joules=5.0,
action_energy_breakdown=[
{
"action_type": "lm_inference",
"duration_s": 1.5,
"gpu_energy_joules": 4.0,
"cpu_energy_joules": 0.3,
},
{
"action_type": "tool_call:search",
"duration_s": 0.5,
"gpu_energy_joules": 1.0,
"cpu_energy_joules": 0.1,
},
],
),
],
total_wall_clock_s=2.0,
completed=True,
))
traces.append(
QueryTrace(
query_id=f"q{i:04d}",
workload_type="test",
turns=[
TurnTrace(
turn_index=0,
input_tokens=100,
output_tokens=50,
wall_clock_s=2.0,
gpu_energy_joules=5.0,
action_energy_breakdown=[
{
"action_type": "lm_inference",
"duration_s": 1.5,
"gpu_energy_joules": 4.0,
"cpu_energy_joules": 0.3,
},
{
"action_type": "tool_call:search",
"duration_s": 0.5,
"gpu_energy_joules": 1.0,
"cpu_energy_joules": 0.1,
},
],
),
],
total_wall_clock_s=2.0,
completed=True,
)
)
path = tmp_path / "summary.json"
export_summary_json(traces, {}, path)
summary = json.loads(path.read_text())
+98 -45
View File
@@ -55,13 +55,18 @@ def _db_record(direct=None, md5=None, sql="SELECT * FROM users", skills=None):
answer_info = {"direct": direct, "md5": md5, "sql": sql}
answer_type = "md5" if md5 else "direct"
return EvalRecord(
record_id="test-1", problem="task",
record_id="test-1",
problem="task",
reference=json.dumps(answer_info),
category="agentic", subject=f"db_{answer_type}",
category="agentic",
subject=f"db_{answer_type}",
metadata={
"answer_info": answer_info, "answer_type": answer_type,
"skills": skills or [], "table_info": _TABLE_INFO,
"table_name": "users", "subset": "db_bench",
"answer_info": answer_info,
"answer_type": answer_type,
"skills": skills or [],
"table_info": _TABLE_INFO,
"table_name": "users",
"subset": "db_bench",
"sample_index": 0,
},
)
@@ -69,9 +74,11 @@ def _db_record(direct=None, md5=None, sql="SELECT * FROM users", skills=None):
def _kg_record(answer_list=None, skills=None, action_list=None):
return EvalRecord(
record_id="test-kg-1", problem="question",
record_id="test-kg-1",
problem="question",
reference=json.dumps(answer_list or []),
category="agentic", subject="knowledge_graph",
category="agentic",
subject="knowledge_graph",
metadata={
"subset": "knowledge_graph",
"question": "What is the answer?",
@@ -86,9 +93,11 @@ def _kg_record(answer_list=None, skills=None, action_list=None):
def _os_record():
return EvalRecord(
record_id="test-os-1", problem="task",
record_id="test-os-1",
problem="task",
reference="{}",
category="agentic", subject="os_interaction",
category="agentic",
subject="os_interaction",
metadata={
"subset": "os_interaction",
"instruction": "Create a file",
@@ -113,6 +122,7 @@ def _os_record():
# DB building
# ---------------------------------------------------------------------------
class TestBuildDB:
def test_creates_table_with_rows(self) -> None:
conn = build_db(_TABLE_INFO)
@@ -174,6 +184,7 @@ class TestTableStateComparison:
# SQL extraction (original's Action: Operation format)
# ---------------------------------------------------------------------------
class TestExtractSQL:
def test_action_operation_format(self) -> None:
text = "Action: Operation\n```sql\nSELECT * FROM users;\n```"
@@ -211,6 +222,7 @@ class TestExtractSQL:
# Text answer parsing (DirectTypeAnswerValidator format)
# ---------------------------------------------------------------------------
class TestTextAnswerParsing:
def test_tuple_list(self) -> None:
text = "Final Answer: [(1, 'Alice', 95.5), (2, 'Bob', 87.0)]"
@@ -249,6 +261,7 @@ class TestTextAnswerParsing:
# DB scorer: direct (SELECT)
# ---------------------------------------------------------------------------
class TestScorerDBDirect:
def test_correct_sql(self) -> None:
s = LifelongAgentScorer()
@@ -265,7 +278,8 @@ class TestScorerDBDirect:
direct=[[1, "Alice", 95.5], [2, "Bob", 87.0], [3, "Carol", 92.3]],
)
ok, meta = s.score(
r, "Action: Operation\n```sql\nSELECT * FROM users\n```",
r,
"Action: Operation\n```sql\nSELECT * FROM users\n```",
)
assert ok is True
@@ -296,7 +310,8 @@ class TestScorerDBDirect:
def test_no_sql_in_response(self) -> None:
ok, meta = LifelongAgentScorer().score(
_db_record(direct=[[1]]), "I don't know",
_db_record(direct=[[1]]),
"I don't know",
)
assert ok is False
@@ -304,7 +319,8 @@ class TestScorerDBDirect:
s = LifelongAgentScorer()
r = _db_record(direct=[[1, "Alice", 95.5]])
ok, meta = s.score(
r, "Action: Answer\nFinal Answer: [(1, 'Alice', 95.5)]",
r,
"Action: Answer\nFinal Answer: [(1, 'Alice', 95.5)]",
)
assert ok is True
assert meta["strategy"] == "text_answer_parsing"
@@ -322,6 +338,7 @@ class TestScorerDBDirect:
# DB scorer: md5 (INSERT/UPDATE/DELETE)
# ---------------------------------------------------------------------------
class TestScorerDBMD5:
def test_correct_insert(self) -> None:
s = LifelongAgentScorer()
@@ -363,6 +380,7 @@ class TestScorerDBMD5:
# KG scorer
# ---------------------------------------------------------------------------
class TestScorerKG:
def test_single_shot_unscorable(self) -> None:
"""KG tasks should be unscorable in single-shot mode."""
@@ -399,6 +417,7 @@ class TestScorerKG:
# OS scorer
# ---------------------------------------------------------------------------
class TestScorerOS:
def test_returns_scorable_status(self) -> None:
s = LifelongAgentScorer()
@@ -413,6 +432,7 @@ class TestScorerOS:
# KG answer extraction
# ---------------------------------------------------------------------------
class TestExtractKGAnswers:
def test_entity_id(self) -> None:
assert extract_kg_answers("Final Answer: m.02h8b9t") == ["m.02h8b9t"]
@@ -441,6 +461,7 @@ class TestExtractKGAnswers:
# Bash command extraction
# ---------------------------------------------------------------------------
class TestExtractBashCommands:
def test_act_format(self) -> None:
text = "Act: ```bash\nls -la /tmp\n```"
@@ -469,6 +490,7 @@ class TestExtractBashCommands:
# Value comparison
# ---------------------------------------------------------------------------
class TestValueComparison:
def test_int(self) -> None:
assert values_match(42, 42)
@@ -512,6 +534,7 @@ class TestTupleComparison:
# Episode grouping
# ---------------------------------------------------------------------------
class TestEpisodeGrouping:
def test_single_subset_episode(self) -> None:
ds = LifelongAgentDataset(subset="db_bench")
@@ -536,14 +559,18 @@ class TestEpisodeGrouping:
ds._records = [
EvalRecord(
record_id="lifelong-db-0",
problem="task", reference="{}",
category="agentic", subject="db_direct",
problem="task",
reference="{}",
category="agentic",
subject="db_direct",
metadata={"subset": "db_bench", "sample_index": 0},
),
EvalRecord(
record_id="lifelong-kg-0",
problem="task", reference="{}",
category="agentic", subject="knowledge_graph",
problem="task",
reference="{}",
category="agentic",
subject="knowledge_graph",
metadata={"subset": "knowledge_graph", "sample_index": 0},
),
]
@@ -555,8 +582,10 @@ class TestEpisodeGrouping:
ds._records = [
EvalRecord(
record_id=f"lifelong-db-{i}",
problem="task", reference="{}",
category="agentic", subject="db_direct",
problem="task",
reference="{}",
category="agentic",
subject="db_direct",
metadata={"subset": "db_bench", "sample_index": i},
)
for i in range(5)
@@ -571,6 +600,7 @@ class TestEpisodeGrouping:
# Dataset
# ---------------------------------------------------------------------------
class TestDataset:
def test_instantiation_default(self) -> None:
ds = LifelongAgentDataset()
@@ -603,6 +633,7 @@ class TestDataset:
# Multi-turn environments
# ---------------------------------------------------------------------------
class TestDBEnvironment:
def test_multi_turn_select(self) -> None:
"""DB environment should handle multi-turn SQL interaction."""
@@ -643,15 +674,15 @@ class TestDBEnvironment:
from openjarvis.evals.environments.lifelong_agent_env import DBEnvironment
record = _db_record(
md5="x", sql="INSERT INTO users VALUES (4, 'Dave', 88.0)",
md5="x",
sql="INSERT INTO users VALUES (4, 'Dave', 88.0)",
)
env = DBEnvironment(use_mysql=False)
env.reset(record)
# Agent executes the correct INSERT
obs, done = env.step(
"Action: Operation\n```sql\n"
"INSERT INTO users VALUES (4, 'Dave', 88.0)\n```"
"Action: Operation\n```sql\nINSERT INTO users VALUES (4, 'Dave', 88.0)\n```"
)
assert not done
assert "successfully" in obs.lower() or "Result" in obs
@@ -814,20 +845,24 @@ class TestOSEnvironment:
# CLI wiring
# ---------------------------------------------------------------------------
class TestCLI:
def test_in_benchmarks(self) -> None:
from openjarvis.evals.cli import BENCHMARKS
assert "lifelong-agent" in BENCHMARKS
assert BENCHMARKS["lifelong-agent"]["category"] == "agentic"
def test_build_dataset(self) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset("lifelong-agent")
assert ds.dataset_id == "lifelong-agent"
assert hasattr(ds, "create_task_env")
def test_build_scorer(self) -> None:
from openjarvis.evals.cli import _build_scorer
s = _build_scorer("lifelong-agent", None, "test-model")
assert s.scorer_id == "lifelong-agent"
@@ -836,9 +871,11 @@ class TestCLI:
# Runner episode_mode integration
# ---------------------------------------------------------------------------
class TestRunnerEpisodeMode:
def test_episode_mode_field_exists(self) -> None:
from openjarvis.evals.core.types import RunConfig
config = RunConfig(
benchmark="lifelong-agent",
backend="jarvis-direct",
@@ -849,6 +886,7 @@ class TestRunnerEpisodeMode:
def test_runner_has_episode_mode_method(self) -> None:
from openjarvis.evals.core.runner import EvalRunner
assert hasattr(EvalRunner, "_run_episode_mode")
assert hasattr(EvalRunner, "_process_interactive")
assert hasattr(EvalRunner, "_inject_examples")
@@ -858,8 +896,11 @@ class TestRunnerEpisodeMode:
from openjarvis.evals.core.runner import EvalRunner
record = EvalRecord(
record_id="test", problem="What is 2+2?",
reference="4", category="reasoning", subject="math",
record_id="test",
problem="What is 2+2?",
reference="4",
category="reasoning",
subject="math",
metadata={},
)
# Call the static-ish method
@@ -872,8 +913,11 @@ class TestRunnerEpisodeMode:
from openjarvis.evals.core.runner import EvalRunner
record = EvalRecord(
record_id="test", problem="What is 2+2?",
reference="4", category="reasoning", subject="math",
record_id="test",
problem="What is 2+2?",
reference="4",
category="reasoning",
subject="math",
metadata={},
)
examples = [{"problem": "What is 1+1?", "answer": "2"}]
@@ -888,20 +932,25 @@ class TestRunnerEpisodeMode:
from openjarvis.evals.core.runner import EvalRunner
record = EvalRecord(
record_id="test", problem="What is 3+3?",
reference="6", category="reasoning", subject="math",
record_id="test",
problem="What is 3+3?",
reference="6",
category="reasoning",
subject="math",
metadata={},
)
examples = [{
"problem": "What is 1+1?",
"answer": "2",
"interaction_history": [
{"role": "user", "content": "What is 1+1?"},
{"role": "assistant", "content": "Action: compute(1+1)"},
{"role": "user", "content": "Result: 2"},
{"role": "assistant", "content": "Final Answer: 2"},
],
}]
examples = [
{
"problem": "What is 1+1?",
"answer": "2",
"interaction_history": [
{"role": "user", "content": "What is 1+1?"},
{"role": "assistant", "content": "Action: compute(1+1)"},
{"role": "user", "content": "Result: 2"},
{"role": "assistant", "content": "Final Answer: 2"},
],
}
]
runner = EvalRunner.__new__(EvalRunner)
result = runner._inject_examples(record, examples)
assert "Previously Completed Tasks" in result.problem
@@ -912,6 +961,7 @@ class TestRunnerEpisodeMode:
def test_max_prior_examples_constant(self) -> None:
"""Runner should have a FIFO buffer size matching original default."""
from openjarvis.evals.core.runner import EvalRunner
assert EvalRunner._MAX_PRIOR_EXAMPLES == 3
@@ -919,14 +969,13 @@ class TestRunnerEpisodeMode:
# KG variable reference resolution
# ---------------------------------------------------------------------------
class TestKGVariableReference:
def test_variable_ref_in_scorer(self) -> None:
"""Scorer should handle Final Answer: #N format."""
# When a variable ref is given and entity IDs are elsewhere in text
result = extract_kg_answers(
"I found the answer.\n"
"The entity m.02h8b9t matches.\n"
"Final Answer: #2"
"I found the answer.\nThe entity m.02h8b9t matches.\nFinal Answer: #2"
)
assert "m.02h8b9t" in result
@@ -962,8 +1011,7 @@ class TestKGVariableReference:
def test_variable_ref_with_var_keyword(self) -> None:
"""Should handle 'Final Answer: Variable #2' format."""
result = extract_kg_answers(
"Based on my analysis, m.001 is the answer.\n"
"Final Answer: Variable #2"
"Based on my analysis, m.001 is the answer.\nFinal Answer: Variable #2"
)
assert "m.001" in result
@@ -972,12 +1020,11 @@ class TestKGVariableReference:
# OS action format
# ---------------------------------------------------------------------------
class TestOSActionFormat:
def test_original_format_act_bash(self) -> None:
"""Should parse original format: Act: bash\\n```bash\\n...\\n```"""
cmds = _extract_bash_commands(
"Act: bash\n```bash\nls -la /tmp\n```"
)
cmds = _extract_bash_commands("Act: bash\n```bash\nls -la /tmp\n```")
assert len(cmds) == 1
assert "ls -la" in cmds[0]
@@ -995,12 +1042,14 @@ class TestOSActionFormat:
# Per-subset max turns
# ---------------------------------------------------------------------------
class TestMaxTurns:
def test_db_max_turns(self) -> None:
from openjarvis.evals.environments.lifelong_agent_env import (
MAX_TURNS_DB,
DBEnvironment,
)
env = DBEnvironment(use_mysql=False)
assert env.max_turns == MAX_TURNS_DB
assert env.max_turns == 3
@@ -1010,6 +1059,7 @@ class TestMaxTurns:
MAX_TURNS_KG,
KGEnvironment,
)
env = KGEnvironment()
assert env.max_turns == MAX_TURNS_KG
assert env.max_turns == 15
@@ -1019,12 +1069,14 @@ class TestMaxTurns:
MAX_TURNS_OS,
OSEnvironment,
)
env = OSEnvironment()
assert env.max_turns == MAX_TURNS_OS
assert env.max_turns == 5
def test_base_default(self) -> None:
from openjarvis.evals.environments.base import TaskEnvironment
# Can't instantiate ABC, but verify the property exists
assert hasattr(TaskEnvironment, "max_turns")
@@ -1033,6 +1085,7 @@ class TestMaxTurns:
# Numeric tolerance
# ---------------------------------------------------------------------------
class TestNumericTolerance:
def test_abs_tol_near_zero(self) -> None:
"""abs_tol=1e-6 should make very small values match zero."""
+19 -8
View File
@@ -101,8 +101,10 @@ class TestLogHubScorer:
def test_exact_match_anomaly(self) -> None:
s = LogHubScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="test-1", problem="analyze logs",
reference="anomaly", category="agentic",
record_id="test-1",
problem="analyze logs",
reference="anomaly",
category="agentic",
)
is_correct, meta = s.score(record, "ANOMALY\nThe logs show errors.")
assert is_correct is True
@@ -111,8 +113,10 @@ class TestLogHubScorer:
def test_exact_match_normal(self) -> None:
s = LogHubScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="test-2", problem="analyze logs",
reference="normal", category="agentic",
record_id="test-2",
problem="analyze logs",
reference="normal",
category="agentic",
)
is_correct, meta = s.score(record, "NORMAL - no issues detected")
assert is_correct is True
@@ -120,8 +124,10 @@ class TestLogHubScorer:
def test_empty_response(self) -> None:
s = LogHubScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="test-3", problem="analyze logs",
reference="anomaly", category="agentic",
record_id="test-3",
problem="analyze logs",
reference="anomaly",
category="agentic",
)
is_correct, meta = s.score(record, "")
assert is_correct is False
@@ -130,8 +136,10 @@ class TestLogHubScorer:
def test_wrong_classification(self) -> None:
s = LogHubScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="test-4", problem="analyze logs",
reference="anomaly", category="agentic",
record_id="test-4",
problem="analyze logs",
reference="anomaly",
category="agentic",
)
is_correct, meta = s.score(record, "NORMAL - everything looks fine")
assert is_correct is False
@@ -140,17 +148,20 @@ class TestLogHubScorer:
class TestLogHubCLI:
def test_in_benchmarks_dict(self) -> None:
from openjarvis.evals.cli import BENCHMARKS
assert "loghub" in BENCHMARKS
assert BENCHMARKS["loghub"]["category"] == "agentic"
def test_build_dataset(self) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset("loghub")
assert ds is not None
assert ds.dataset_id == "loghub"
def test_build_scorer(self) -> None:
from openjarvis.evals.cli import _build_scorer
s = _build_scorer("loghub", _mock_backend(), "test-model")
assert s is not None
assert s.scorer_id == "loghub"
+7 -2
View File
@@ -91,8 +91,10 @@ class TestPaperArenaScorer:
def test_empty_response(self) -> None:
s = PaperArenaScorer(_mock_backend(), "test-model")
record = EvalRecord(
record_id="pa-5", problem="q",
reference="a", category="agentic",
record_id="pa-5",
problem="q",
reference="a",
category="agentic",
)
is_correct, meta = s.score(record, "")
assert is_correct is False
@@ -102,14 +104,17 @@ class TestPaperArenaScorer:
class TestPaperArenaCLI:
def test_in_benchmarks(self) -> None:
from openjarvis.evals.cli import BENCHMARKS
assert "paperarena" in BENCHMARKS
def test_build_dataset(self) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset("paperarena")
assert ds.dataset_id == "paperarena"
def test_build_scorer(self) -> None:
from openjarvis.evals.cli import _build_scorer
s = _build_scorer("paperarena", _mock_backend(), "test-model")
assert s.scorer_id == "paperarena"
+21 -10
View File
@@ -15,6 +15,7 @@ from openjarvis.evals.core.types import EvalResult, RunConfig, RunSummary
# Test double
# ---------------------------------------------------------------------------
class RecordingTracker(ResultTracker):
"""Records all lifecycle calls for testing."""
@@ -58,6 +59,7 @@ class CrashingTracker(ResultTracker):
# Helpers
# ---------------------------------------------------------------------------
def _make_config(**overrides) -> RunConfig:
defaults = dict(benchmark="test", backend="jarvis-direct", model="test-model")
defaults.update(overrides)
@@ -92,6 +94,7 @@ def _make_result(**overrides) -> EvalResult:
# RecordingTracker through EvalRunner lifecycle
# ---------------------------------------------------------------------------
class TestRecordingTrackerIntegration:
"""Test that trackers receive all lifecycle calls through EvalRunner."""
@@ -111,11 +114,13 @@ class TestRecordingTrackerIntegration:
dataset.iter_records = MagicMock(return_value=[record])
backend = MagicMock()
backend.generate_full = MagicMock(return_value={
"content": "2",
"usage": {"prompt_tokens": 10, "completion_tokens": 5},
"latency_seconds": 0.5,
})
backend.generate_full = MagicMock(
return_value={
"content": "2",
"usage": {"prompt_tokens": 10, "completion_tokens": 5},
"latency_seconds": 0.5,
}
)
scorer = MagicMock()
scorer.score = MagicMock(return_value=(True, {}))
@@ -152,11 +157,13 @@ class TestRecordingTrackerIntegration:
dataset.iter_records = MagicMock(return_value=[record])
backend = MagicMock()
backend.generate_full = MagicMock(return_value={
"content": "yes",
"usage": {},
"latency_seconds": 0.1,
})
backend.generate_full = MagicMock(
return_value={
"content": "yes",
"usage": {},
"latency_seconds": 0.1,
}
)
scorer = MagicMock()
scorer.score = MagicMock(return_value=(True, {}))
@@ -178,6 +185,7 @@ class TestRecordingTrackerIntegration:
# WandbTracker unit tests
# ---------------------------------------------------------------------------
class TestWandbTracker:
"""Unit tests for WandbTracker (mocked wandb module)."""
@@ -185,6 +193,7 @@ class TestWandbTracker:
"""WandbTracker raises ImportError when wandb is not installed."""
with patch.dict(sys.modules, {"wandb": None}):
import openjarvis.evals.trackers.wandb_tracker as wt_mod
original = wt_mod.wandb
wt_mod.wandb = None
try:
@@ -277,12 +286,14 @@ class TestWandbTracker:
# SheetsTracker unit tests
# ---------------------------------------------------------------------------
class TestSheetsTracker:
"""Unit tests for SheetsTracker."""
def test_import_error_when_gspread_missing(self):
"""SheetsTracker raises ImportError when gspread not installed."""
import openjarvis.evals.trackers.sheets_tracker as st_mod
original = st_mod.gspread
st_mod.gspread = None
try:
+56 -23
View File
@@ -25,12 +25,14 @@ import pytest
class TestEmailTriageDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
assert ds.dataset_id == "email_triage"
assert ds.dataset_name == "Email Triage"
def test_load(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
@@ -44,6 +46,7 @@ class TestEmailTriageDataset:
def test_load_all(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
ds.load()
assert ds.size() == 30
@@ -52,12 +55,14 @@ class TestEmailTriageDataset:
class TestMorningBriefDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
assert ds.dataset_id == "morning_brief"
assert ds.dataset_name == "Morning Brief"
def test_load(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
@@ -68,6 +73,7 @@ class TestMorningBriefDataset:
def test_load_all(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
ds.load()
assert ds.size() == 15
@@ -76,12 +82,14 @@ class TestMorningBriefDataset:
class TestResearchMiningDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
assert ds.dataset_id == "research_mining"
assert ds.dataset_name == "Research Mining"
def test_load(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
@@ -92,6 +100,7 @@ class TestResearchMiningDataset:
def test_load_all(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
ds.load()
assert ds.size() == 31
@@ -100,12 +109,14 @@ class TestResearchMiningDataset:
class TestKnowledgeBaseDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
assert ds.dataset_id == "knowledge_base"
assert ds.dataset_name == "Knowledge Base"
def test_load(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
@@ -116,6 +127,7 @@ class TestKnowledgeBaseDataset:
def test_load_all(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
ds.load()
assert ds.size() == 30
@@ -124,12 +136,14 @@ class TestKnowledgeBaseDataset:
class TestCodingTaskDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
assert ds.dataset_id == "coding_task"
assert ds.dataset_name == "Coding Task"
def test_load(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
@@ -141,6 +155,7 @@ class TestCodingTaskDataset:
def test_load_all(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
ds.load()
assert ds.size() == 29
@@ -157,26 +172,31 @@ class TestScorerInstantiation:
def test_email_triage_scorer(self) -> None:
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
scorer = EmailTriageScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "email_triage"
def test_morning_brief_scorer(self) -> None:
from openjarvis.evals.scorers.morning_brief import MorningBriefScorer
scorer = MorningBriefScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "morning_brief"
def test_research_mining_scorer(self) -> None:
from openjarvis.evals.scorers.research_mining import ResearchMiningScorer
scorer = ResearchMiningScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "research_mining"
def test_knowledge_base_scorer(self) -> None:
from openjarvis.evals.scorers.knowledge_base import KnowledgeBaseScorer
scorer = KnowledgeBaseScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "knowledge_base"
def test_coding_task_scorer(self) -> None:
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
scorer = CodingTaskScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "coding_task"
@@ -208,10 +228,7 @@ class TestCodingTaskScoring:
),
},
)
answer = (
"def is_palindrome(s):\n"
" return s == s[::-1]"
)
answer = "def is_palindrome(s):\n return s == s[::-1]"
is_correct, meta = scorer.score(record, answer)
assert is_correct is True
assert meta["tests_passed"] == 3
@@ -228,10 +245,7 @@ class TestCodingTaskScoring:
reference="",
category="use-case",
metadata={
"test_cases": (
"assert add(1, 2) == 3\n"
"assert add(0, 0) == 0"
),
"test_cases": ("assert add(1, 2) == 3\nassert add(0, 0) == 0"),
},
)
# Wrong implementation
@@ -285,27 +299,35 @@ class TestEmailTriageScoring:
class TestCLIFactories:
"""Test that _build_dataset and _build_scorer work for new benchmarks."""
@pytest.mark.parametrize("benchmark", [
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
])
@pytest.mark.parametrize(
"benchmark",
[
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
],
)
def test_build_dataset(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset(benchmark)
assert ds.dataset_id == benchmark
@pytest.mark.parametrize("benchmark", [
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
])
@pytest.mark.parametrize(
"benchmark",
[
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
],
)
def test_build_scorer(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_scorer
scorer = _build_scorer(benchmark, MagicMock(), "gpt-5-mini")
assert scorer.scorer_id == benchmark
@@ -320,6 +342,7 @@ class TestCostCalculator:
def test_estimate_monthly_cost(self) -> None:
from openjarvis.server.cost_calculator import estimate_monthly_cost
est = estimate_monthly_cost(
calls_per_month=1000,
avg_input_tokens=500,
@@ -332,6 +355,7 @@ class TestCostCalculator:
def test_estimate_scenario(self) -> None:
from openjarvis.server.cost_calculator import estimate_scenario
estimates = estimate_scenario("daily_briefing")
assert len(estimates) == 3 # 3 cloud providers
for est in estimates:
@@ -339,16 +363,19 @@ class TestCostCalculator:
def test_estimate_all_scenarios(self) -> None:
from openjarvis.server.cost_calculator import estimate_all_scenarios
all_est = estimate_all_scenarios()
assert len(all_est) == 5 # 5 scenarios
def test_unknown_provider(self) -> None:
from openjarvis.server.cost_calculator import estimate_monthly_cost
with pytest.raises(ValueError, match="Unknown provider"):
estimate_monthly_cost(100, 100, 100, "nonexistent")
def test_unknown_scenario(self) -> None:
from openjarvis.server.cost_calculator import estimate_scenario
with pytest.raises(ValueError, match="Unknown scenario"):
estimate_scenario("nonexistent")
@@ -363,6 +390,7 @@ class TestSavings:
def test_compute_savings_basic(self) -> None:
from openjarvis.server.savings import compute_savings
summary = compute_savings(1000, 500, total_calls=10)
assert summary.total_calls == 10
assert summary.total_tokens == 1500
@@ -375,15 +403,20 @@ class TestSavings:
import time
from openjarvis.server.savings import compute_savings
start = time.time() - 3600 # 1 hour ago
summary = compute_savings(
100000, 50000, total_calls=100, session_start=start,
100000,
50000,
total_calls=100,
session_start=start,
)
assert summary.session_duration_hours > 0
assert summary.monthly_projection # not empty
def test_savings_to_dict(self) -> None:
from openjarvis.server.savings import compute_savings, savings_to_dict
summary = compute_savings(1000, 500, total_calls=5)
d = savings_to_dict(summary)
assert isinstance(d, dict)
+12 -8
View File
@@ -28,9 +28,9 @@ class TestAMDDetection:
@patch(
"openjarvis.core.config._run_cmd",
side_effect=[
"AMD Instinct MI300X", # --showproductname
"AMD Instinct MI300X", # --showproductname
"GPU[0] : vram Total Memory (B): 206158430208", # --showmeminfo vram
"GPU[0] : Some info", # --showallinfo
"GPU[0] : Some info", # --showallinfo
],
)
def test_rocm_smi_parsing(self, mock_run, mock_which):
@@ -48,8 +48,8 @@ class TestAMDDetection:
"openjarvis.core.config._run_cmd",
side_effect=[
"AMD Instinct MI250X\nAMD Instinct MI250X", # --showproductname
"", # --showmeminfo vram (empty)
"", # --showallinfo (empty)
"", # --showmeminfo vram (empty)
"", # --showallinfo (empty)
],
)
def test_amd_gpu_model(self, mock_run, mock_which):
@@ -140,8 +140,10 @@ class TestAMDEngineRecommendation:
cpu_count=96,
ram_gb=768.0,
gpu=GpuInfo(
vendor="amd", name="AMD Instinct MI300X",
vram_gb=192.0, count=1,
vendor="amd",
name="AMD Instinct MI300X",
vram_gb=192.0,
count=1,
),
)
assert recommend_engine(hw) == "vllm"
@@ -163,8 +165,10 @@ class TestAMDEngineRecommendation:
cpu_count=128,
ram_gb=1024.0,
gpu=GpuInfo(
vendor="amd", name="AMD Instinct MI300X",
vram_gb=192.0, count=4,
vendor="amd",
name="AMD Instinct MI300X",
vram_gb=192.0,
count=4,
),
)
assert recommend_engine(hw) == "vllm"
+1 -5
View File
@@ -87,11 +87,7 @@ class TestAppleDetection:
@patch("openjarvis.core.config.platform.system", return_value="Darwin")
@patch(
"openjarvis.core.config._run_cmd",
return_value=(
"Graphics/Displays:\n"
" Apple Silicon\n"
" Type: GPU\n"
),
return_value=("Graphics/Displays:\n Apple Silicon\n Type: GPU\n"),
)
def test_apple_no_chipset_line_falls_back(self, mock_run, mock_system):
"""When no 'Chipset Model' line exists, falls back to 'Apple Silicon'."""
+2 -2
View File
@@ -38,9 +38,9 @@ class TestDetectHardware:
@patch(
"openjarvis.core.config._run_cmd",
side_effect=[
"AMD Instinct MI300X", # --showproductname
"AMD Instinct MI300X", # --showproductname
"GPU[0] : vram Total Memory (B): 206158430208", # --showmeminfo vram
"GPU[0] : Some info", # --showallinfo
"GPU[0] : Some info", # --showallinfo
],
)
def test_detect_amd_gpu(self, mock_run, mock_which):
+8 -4
View File
@@ -104,7 +104,8 @@ class TestNVIDIAEngineRecommendation:
gpu=GpuInfo(
vendor="nvidia",
name="NVIDIA A100-SXM4-80GB",
vram_gb=80.0, count=1,
vram_gb=80.0,
count=1,
),
)
assert recommend_engine(hw) == "vllm"
@@ -118,7 +119,8 @@ class TestNVIDIAEngineRecommendation:
gpu=GpuInfo(
vendor="nvidia",
name="NVIDIA H100 80GB HBM3",
vram_gb=80.0, count=1,
vram_gb=80.0,
count=1,
),
)
assert recommend_engine(hw) == "vllm"
@@ -132,7 +134,8 @@ class TestNVIDIAEngineRecommendation:
gpu=GpuInfo(
vendor="nvidia",
name="NVIDIA Tesla V100-SXM2-32GB",
vram_gb=32.0, count=1,
vram_gb=32.0,
count=1,
),
)
assert recommend_engine(hw) == "ollama"
@@ -146,7 +149,8 @@ class TestNVIDIAEngineRecommendation:
gpu=GpuInfo(
vendor="nvidia",
name="NVIDIA GeForce RTX 4090",
vram_gb=24.0, count=1,
vram_gb=24.0,
count=1,
),
)
assert recommend_engine(hw) == "ollama"
+4 -4
View File
@@ -25,9 +25,7 @@ class TestResearchMonitorE2E:
templates = manager.list_templates()
assert any(t["id"] == "research_monitor" for t in templates)
agent = manager.create_from_template(
"research_monitor", "My Researcher"
)
agent = manager.create_from_template("research_monitor", "My Researcher")
assert agent["name"] == "My Researcher"
assert agent["agent_type"] == "monitor_operative"
assert agent["status"] == "idle"
@@ -64,7 +62,9 @@ class TestResearchMonitorE2E:
# Complete task
manager.update_task(
t1["id"], status="completed", findings=["Paper A", "Paper B"],
t1["id"],
status="completed",
findings=["Paper A", "Paper B"],
)
task = manager._get_task(t1["id"])
assert task["status"] == "completed"
+55 -31
View File
@@ -138,7 +138,8 @@ class TestOrchestratorWithCalculator:
bus = EventBus(record_history=True)
agent_cls = AgentRegistry.get("orchestrator")
agent = agent_cls(
engine, "test-model",
engine,
"test-model",
tools=[CalculatorTool()],
bus=bus,
)
@@ -250,8 +251,7 @@ class TestTelemetryThroughAgent:
agent.run("Hello")
telem_events = [
e for e in bus.history
if e.event_type == EventType.TELEMETRY_RECORD
e for e in bus.history if e.event_type == EventType.TELEMETRY_RECORD
]
assert len(telem_events) == 1
rec = telem_events[0].data["record"]
@@ -286,14 +286,8 @@ class TestToolExecutorIntegration:
assert think_result.content == "Step 1: solve"
# Verify events
starts = [
e for e in bus.history
if e.event_type == EventType.TOOL_CALL_START
]
ends = [
e for e in bus.history
if e.event_type == EventType.TOOL_CALL_END
]
starts = [e for e in bus.history if e.event_type == EventType.TOOL_CALL_START]
ends = [e for e in bus.history if e.event_type == EventType.TOOL_CALL_END]
assert len(starts) == 2
assert len(ends) == 2
@@ -321,7 +315,9 @@ class TestHeuristicRewardWithTelemetry:
)
rf = HeuristicRewardFunction()
score = rf.compute(
RoutingContext(query="test"), rec.model_id, "response",
RoutingContext(query="test"),
rec.model_id,
"response",
latency_seconds=rec.latency_seconds,
cost_usd=rec.cost_usd,
prompt_tokens=rec.prompt_tokens,
@@ -352,16 +348,30 @@ class TestTelemetryPipeline:
db = tmp_path / "telemetry.db"
store = TelemetryStore(db)
store.record(TelemetryRecord(
timestamp=time.time(), model_id="m1", engine="ollama",
prompt_tokens=10, completion_tokens=5, total_tokens=15,
latency_seconds=1.0, cost_usd=0.001,
))
store.record(TelemetryRecord(
timestamp=time.time(), model_id="m2", engine="vllm",
prompt_tokens=20, completion_tokens=10, total_tokens=30,
latency_seconds=0.5, cost_usd=0.002,
))
store.record(
TelemetryRecord(
timestamp=time.time(),
model_id="m1",
engine="ollama",
prompt_tokens=10,
completion_tokens=5,
total_tokens=15,
latency_seconds=1.0,
cost_usd=0.001,
)
)
store.record(
TelemetryRecord(
timestamp=time.time(),
model_id="m2",
engine="vllm",
prompt_tokens=20,
completion_tokens=10,
total_tokens=30,
latency_seconds=0.5,
cost_usd=0.002,
)
)
store.close()
agg = TelemetryAggregator(db)
@@ -387,8 +397,12 @@ class TestEventBusTelemetryAggregator:
# Publish a telemetry event
rec = TelemetryRecord(
timestamp=1000.0, model_id="event-model", engine="test",
prompt_tokens=5, completion_tokens=3, total_tokens=8,
timestamp=1000.0,
model_id="event-model",
engine="test",
prompt_tokens=5,
completion_tokens=3,
total_tokens=8,
latency_seconds=0.1,
)
bus.publish(EventType.TELEMETRY_RECORD, {"record": rec})
@@ -435,11 +449,18 @@ class TestRewardTelemetryIntegration:
db = tmp_path / "telemetry.db"
store = TelemetryStore(db)
store.record(TelemetryRecord(
timestamp=time.time(), model_id="scored-model", engine="test",
prompt_tokens=50, completion_tokens=25, total_tokens=75,
latency_seconds=3.0, cost_usd=0.003,
))
store.record(
TelemetryRecord(
timestamp=time.time(),
model_id="scored-model",
engine="test",
prompt_tokens=50,
completion_tokens=25,
total_tokens=75,
latency_seconds=3.0,
cost_usd=0.003,
)
)
store.close()
agg = TelemetryAggregator(db)
@@ -448,7 +469,9 @@ class TestRewardTelemetryIntegration:
rf = HeuristicRewardFunction()
score = rf.compute(
RoutingContext(query="test"), ms.model_id, "response",
RoutingContext(query="test"),
ms.model_id,
"response",
latency_seconds=ms.avg_latency,
cost_usd=ms.total_cost,
prompt_tokens=ms.prompt_tokens,
@@ -581,7 +604,8 @@ class TestFullPipeline:
def run(self, input, context=None, **kwargs):
result = self.engine.generate(
[], model=self.model,
[],
model=self.model,
)
return AgentResult(content=result["content"], turns=1)
+48 -60
View File
@@ -87,16 +87,15 @@ class TestReActPipeline:
"Action: calculator\n"
'Action Input: {"expression":"2+2"}'
),
_simple_response(
"Thought: The result is 4.\n"
"Final Answer: 2+2 equals 4."
),
_simple_response("Thought: The result is 4.\nFinal Answer: 2+2 equals 4."),
]
engine = _make_engine(responses)
bus = EventBus(record_history=True)
agent = NativeReActAgent(
engine, "test-model",
tools=[CalculatorTool()], bus=bus,
engine,
"test-model",
tools=[CalculatorTool()],
bus=bus,
)
result = agent.run("What is 2+2?")
@@ -118,13 +117,14 @@ class TestReActPipeline:
'Action Input: {"thought":"Step 1: analyze"}'
),
_simple_response(
"Thought: I have my analysis.\n"
"Final Answer: The answer is clear."
"Thought: I have my analysis.\nFinal Answer: The answer is clear."
),
]
engine = _make_engine(responses)
agent = NativeReActAgent(
engine, "test-model", tools=[ThinkTool()],
engine,
"test-model",
tools=[ThinkTool()],
)
result = agent.run("Analyze this.")
assert result.turns == 2
@@ -136,10 +136,7 @@ class TestReActPipeline:
from openjarvis.agents.native_react import NativeReActAgent
engine = _make_engine(
_simple_response(
"Thought: This is simple.\n"
"Final Answer: Hello!"
)
_simple_response("Thought: This is simple.\nFinal Answer: Hello!")
)
agent = NativeReActAgent(engine, "test-model")
result = agent.run("Say hello")
@@ -155,14 +152,12 @@ class TestReActPipeline:
_register_all()
from openjarvis.agents.native_react import NativeReActAgent
engine = _make_engine(
_simple_response(
"Thought: done.\nFinal Answer: ok"
)
)
engine = _make_engine(_simple_response("Thought: done.\nFinal Answer: ok"))
bus = EventBus(record_history=True)
agent = NativeReActAgent(
engine, "test-model", bus=bus,
engine,
"test-model",
bus=bus,
)
agent.run("Test")
@@ -188,19 +183,18 @@ class TestOpenHandsPipeline:
if not ToolRegistry.contains("code_interpreter"):
ToolRegistry.register_value(
"code_interpreter", CodeInterpreterTool,
"code_interpreter",
CodeInterpreterTool,
)
responses = [
_simple_response(
"I'll calculate this:\n"
"```python\nprint(2 + 2)\n```"
),
_simple_response("I'll calculate this:\n```python\nprint(2 + 2)\n```"),
_simple_response("The result is 4."),
]
engine = _make_engine(responses)
agent = NativeOpenHandsAgent(
engine, "test-model",
engine,
"test-model",
tools=[CodeInterpreterTool()],
)
result = agent.run("What is 2+2?")
@@ -216,9 +210,7 @@ class TestOpenHandsPipeline:
_register_all()
from openjarvis.agents.native_openhands import NativeOpenHandsAgent
engine = _make_engine(
_simple_response("Hello! How can I help?")
)
engine = _make_engine(_simple_response("Hello! How can I help?"))
agent = NativeOpenHandsAgent(engine, "test-model")
result = agent.run("Say hello")
assert result.content == "Hello! How can I help?"
@@ -229,12 +221,12 @@ class TestOpenHandsPipeline:
_register_all()
from openjarvis.agents.native_openhands import NativeOpenHandsAgent
engine = _make_engine(
_simple_response("Direct answer.")
)
engine = _make_engine(_simple_response("Direct answer."))
bus = EventBus(record_history=True)
agent = NativeOpenHandsAgent(
engine, "test-model", bus=bus,
engine,
"test-model",
bus=bus,
)
agent.run("Test")
@@ -275,14 +267,16 @@ class TestMCPIntegration:
# Call calculator
result = client.call_tool(
"calculator", {"expression": "10*5"},
"calculator",
{"expression": "10*5"},
)
assert result["content"][0]["text"] == "50.0"
assert result["isError"] is False
# Call think
result = client.call_tool(
"think", {"thought": "reasoning step"},
"think",
{"thought": "reasoning step"},
)
assert result["content"][0]["text"] == "reasoning step"
@@ -325,7 +319,8 @@ class TestMCPIntegration:
# 3. Call tool
result = client.call_tool(
"calculator", {"expression": "7+3"},
"calculator",
{"expression": "7+3"},
)
assert result["content"][0]["text"] == "10.0"
@@ -372,15 +367,13 @@ class TestCrossEngineConsistency:
"Action: calculator\n"
'Action Input: {"expression":"3*3"}'
),
_simple_response(
"Thought: got 9.\n"
"Final Answer: 9"
),
_simple_response("Thought: got 9.\nFinal Answer: 9"),
]
engine = _make_engine(responses)
engine.engine_id = engine_name
agent = NativeReActAgent(
engine, "test-model",
engine,
"test-model",
tools=[CalculatorTool()],
)
result = agent.run("What is 3*3?")
@@ -453,13 +446,9 @@ class TestModelCatalogIntegration:
BUILTIN_MODELS,
)
local = [
s for s in BUILTIN_MODELS if not s.requires_api_key
]
local = [s for s in BUILTIN_MODELS if not s.requires_api_key]
for spec in local:
assert len(spec.supported_engines) >= 1, (
f"{spec.model_id} has no engines"
)
assert len(spec.supported_engines) >= 1, f"{spec.model_id} has no engines"
def test_cloud_models_require_api_key(self):
"""All cloud models require an API key."""
@@ -478,9 +467,7 @@ class TestModelCatalogIntegration:
"gemini-3-flash",
]
for mid in cloud_ids:
matches = [
s for s in BUILTIN_MODELS if s.model_id == mid
]
matches = [s for s in BUILTIN_MODELS if s.model_id == mid]
assert len(matches) == 1, f"Missing {mid}"
assert matches[0].requires_api_key is True
@@ -494,7 +481,8 @@ class TestAgentRoutingMatrix:
"""Agents run consistently across different configurations."""
@pytest.mark.parametrize(
"agent_key", ["native_react", "native_openhands"],
"agent_key",
["native_react", "native_openhands"],
)
def test_agent_returns_valid_result(self, agent_key):
_register_all()
@@ -515,7 +503,8 @@ class TestAgentRoutingMatrix:
assert len(result.content) > 0
@pytest.mark.parametrize(
"agent_key", ["native_react", "native_openhands"],
"agent_key",
["native_react", "native_openhands"],
)
def test_agent_emits_events(self, agent_key):
_register_all()
@@ -543,15 +532,16 @@ class TestAgentRoutingMatrix:
engine = _make_engine(
_simple_response(
"Thought: I see the system message.\n"
"Final Answer: Got context."
"Thought: I see the system message.\nFinal Answer: Got context."
)
)
conv = Conversation()
conv.add(Message(
role=Role.SYSTEM,
content="You are helpful.",
))
conv.add(
Message(
role=Role.SYSTEM,
content="You are helpful.",
)
)
ctx = AgentContext(conversation=conv)
agent = NativeReActAgent(engine, "test-model")
result = agent.run("Hello", context=ctx)
@@ -561,6 +551,4 @@ class TestAgentRoutingMatrix:
call_args = engine.generate.call_args
msgs = call_args[0][0]
# First message is ReAct system prompt, then context
assert any(
m.content == "You are helpful." for m in msgs
)
assert any(m.content == "You are helpful." for m in msgs)
@@ -39,7 +39,8 @@ class TestBackwardCompatShims:
ctx = build_routing_context("hello")
assert ctx.query == "hello"
router = HeuristicRouter(
available_models=[], default_model="m",
available_models=[],
default_model="m",
)
assert router.select_model(ctx) == "m"
@@ -16,6 +16,7 @@ from openjarvis.intelligence.model_catalog import (
# Helpers
# ---------------------------------------------------------------------------
def _get_spec(model_id: str) -> ModelSpec:
"""Lookup a spec from the BUILTIN_MODELS list (not registry)."""
for spec in BUILTIN_MODELS:
@@ -273,10 +274,19 @@ class TestModelDiscovery:
def test_cloud_models_require_api_key(self) -> None:
"""All cloud models have requires_api_key=True."""
cloud_ids = {
"gpt-4o", "gpt-4o-mini", "gpt-5-mini", "gpt-5-mini-2025-08-07",
"claude-sonnet-4-20250514", "claude-opus-4-20250514",
"claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5",
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-3-pro", "gemini-3-flash",
"gpt-4o",
"gpt-4o-mini",
"gpt-5-mini",
"gpt-5-mini-2025-08-07",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-3-pro",
"gemini-3-flash",
}
for spec in BUILTIN_MODELS:
if spec.model_id in cloud_ids:
@@ -287,9 +297,15 @@ class TestModelDiscovery:
def test_moe_models_have_active_params(self) -> None:
"""MoE models have active_parameter_count_b set."""
moe_ids = {
"gpt-oss:120b", "glm-4.7-flash", "trinity-mini",
"qwen3.5:3b", "qwen3.5:8b", "qwen3.5:14b",
"qwen3.5:35b", "qwen3.5:122b", "qwen3.5:397b",
"gpt-oss:120b",
"glm-4.7-flash",
"trinity-mini",
"qwen3.5:3b",
"qwen3.5:8b",
"qwen3.5:14b",
"qwen3.5:35b",
"qwen3.5:122b",
"qwen3.5:397b",
"granite4.0-h-small",
}
for spec in BUILTIN_MODELS:
@@ -308,8 +324,12 @@ class TestModelDiscovery:
"""merge_discovered_models works for all new model IDs."""
register_builtin_models()
new_ids = [
"gpt-oss:120b", "glm-4.7-flash", "trinity-mini",
"gpt-5-mini", "claude-opus-4-6", "gemini-3-pro",
"gpt-oss:120b",
"glm-4.7-flash",
"trinity-mini",
"gpt-5-mini",
"claude-opus-4-6",
"gemini-3-pro",
]
# Merging known IDs should not raise
merge_discovered_models("vllm", new_ids)
+8 -4
View File
@@ -19,15 +19,19 @@ def _register_models() -> None:
ModelRegistry.register_value(
"small",
ModelSpec(
model_id="small", name="Small",
parameter_count_b=3.0, context_length=4096,
model_id="small",
name="Small",
parameter_count_b=3.0,
context_length=4096,
),
)
ModelRegistry.register_value(
"large",
ModelSpec(
model_id="large", name="Large",
parameter_count_b=70.0, context_length=131072,
model_id="large",
name="Large",
parameter_count_b=70.0,
context_length=131072,
),
)

Some files were not shown because too many files have changed in this diff Show More