mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
Add ReAct and OpenHands agents, WebSearch and CodeInterpreter tools, full MCP protocol layer (server/client/transport), Gemini cloud engine support, 12 new model specs (4 local MoE + 8 cloud), trace system, and comprehensive test coverage across all dimensions (hardware, engine, memory, agents, tools, MCP, integration). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
"""Tests for the TraceCollector."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent
|
|
from openjarvis.core.events import EventBus, EventType
|
|
from openjarvis.core.types import StepType
|
|
from openjarvis.traces.collector import TraceCollector
|
|
from openjarvis.traces.store import TraceStore
|
|
|
|
|
|
class _FakeAgent(BaseAgent):
|
|
"""Minimal agent that returns a fixed response."""
|
|
|
|
agent_id = "fake"
|
|
|
|
def __init__(
|
|
self,
|
|
response: str = "test response",
|
|
bus: Optional[EventBus] = None,
|
|
) -> None:
|
|
self._response = response
|
|
self._bus = bus
|
|
|
|
def run(
|
|
self, input: str, context: Optional[AgentContext] = None,
|
|
**kwargs: Any,
|
|
) -> AgentResult:
|
|
# Simulate an inference step via event bus
|
|
if self._bus:
|
|
self._bus.publish(EventType.INFERENCE_START, {
|
|
"model": "qwen3:8b",
|
|
"engine": "ollama",
|
|
})
|
|
self._bus.publish(EventType.INFERENCE_END, {
|
|
"total_tokens": 50,
|
|
})
|
|
return AgentResult(content=self._response, turns=1)
|
|
|
|
|
|
class _ToolAgent(BaseAgent):
|
|
"""Agent that simulates a tool call during execution."""
|
|
|
|
agent_id = "tool_agent"
|
|
|
|
def __init__(self, bus: EventBus) -> None:
|
|
self._bus = bus
|
|
|
|
def run(
|
|
self, input: str, context: Optional[AgentContext] = None,
|
|
**kwargs: Any,
|
|
) -> AgentResult:
|
|
# Simulate inference + tool call + inference
|
|
inf = {"model": "qwen3:8b", "engine": "ollama"}
|
|
self._bus.publish(EventType.INFERENCE_START, inf)
|
|
self._bus.publish(EventType.INFERENCE_END, {"total_tokens": 30})
|
|
self._bus.publish(EventType.TOOL_CALL_START, {
|
|
"tool": "calculator", "arguments": {"expr": "2+2"},
|
|
})
|
|
self._bus.publish(EventType.TOOL_CALL_END, {
|
|
"tool": "calculator", "success": True, "latency": 0.01,
|
|
})
|
|
self._bus.publish(EventType.INFERENCE_START, inf)
|
|
self._bus.publish(EventType.INFERENCE_END, {"total_tokens": 20})
|
|
return AgentResult(content="4", turns=2)
|
|
|
|
|
|
class TestTraceCollector:
|
|
def test_basic_collection(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(response="hello", bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
result = collector.run("say hello")
|
|
|
|
assert result.content == "hello"
|
|
assert store.count() == 1
|
|
|
|
traces = store.list_traces()
|
|
trace = traces[0]
|
|
assert trace.query == "say hello"
|
|
assert trace.agent == "fake"
|
|
assert trace.model == "qwen3:8b"
|
|
assert trace.engine == "ollama"
|
|
assert trace.result == "hello"
|
|
store.close()
|
|
|
|
def test_records_generate_steps(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
collector.run("test")
|
|
|
|
trace = store.list_traces()[0]
|
|
generate_steps = [s for s in trace.steps if s.step_type == StepType.GENERATE]
|
|
assert len(generate_steps) == 1
|
|
assert generate_steps[0].output.get("tokens") == 50
|
|
store.close()
|
|
|
|
def test_records_tool_steps(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _ToolAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
collector.run("What is 2+2?")
|
|
|
|
trace = store.list_traces()[0]
|
|
tool_steps = [s for s in trace.steps if s.step_type == StepType.TOOL_CALL]
|
|
assert len(tool_steps) == 1
|
|
assert tool_steps[0].input["tool"] == "calculator"
|
|
assert tool_steps[0].output["success"] is True
|
|
store.close()
|
|
|
|
def test_records_respond_step(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(response="final answer", bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
collector.run("test")
|
|
|
|
trace = store.list_traces()[0]
|
|
respond_steps = [s for s in trace.steps if s.step_type == StepType.RESPOND]
|
|
assert len(respond_steps) == 1
|
|
assert respond_steps[0].output["content"] == "final answer"
|
|
store.close()
|
|
|
|
def test_records_memory_retrieve(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
# Monkey-patch agent to emit memory event
|
|
original_run = agent.run
|
|
|
|
def run_with_memory(input, context=None, **kwargs):
|
|
bus.publish(EventType.MEMORY_RETRIEVE, {
|
|
"query": "meeting notes",
|
|
"num_results": 3,
|
|
"latency": 0.2,
|
|
})
|
|
return original_run(input, context=context, **kwargs)
|
|
|
|
agent.run = run_with_memory
|
|
collector.run("find my meeting notes")
|
|
|
|
trace = store.list_traces()[0]
|
|
retrieve_steps = [s for s in trace.steps if s.step_type == StepType.RETRIEVE]
|
|
assert len(retrieve_steps) == 1
|
|
assert retrieve_steps[0].input["query"] == "meeting notes"
|
|
store.close()
|
|
|
|
def test_publishes_trace_complete(self, tmp_path: Path) -> None:
|
|
bus = EventBus(record_history=True)
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
collector.run("test")
|
|
|
|
trace_events = [
|
|
e for e in bus.history
|
|
if e.event_type == EventType.TRACE_COMPLETE
|
|
]
|
|
assert len(trace_events) == 1
|
|
assert trace_events[0].data["trace"].query == "test"
|
|
store.close()
|
|
|
|
def test_no_store(self) -> None:
|
|
"""Collector works without a store (just collects, doesn't persist)."""
|
|
bus = EventBus()
|
|
agent = _FakeAgent(response="ok", bus=bus)
|
|
collector = TraceCollector(agent, bus=bus) # no store
|
|
|
|
result = collector.run("test")
|
|
assert result.content == "ok"
|
|
|
|
def test_no_bus(self, tmp_path: Path) -> None:
|
|
"""Collector works without a bus (no event-based step collection)."""
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(response="ok")
|
|
collector = TraceCollector(agent, store=store) # no bus
|
|
|
|
result = collector.run("test")
|
|
assert result.content == "ok"
|
|
assert store.count() == 1
|
|
# Only the RESPOND step (no events to capture)
|
|
trace = store.list_traces()[0]
|
|
assert len(trace.steps) == 1
|
|
assert trace.steps[0].step_type == StepType.RESPOND
|
|
store.close()
|
|
|
|
def test_timing(self, tmp_path: Path) -> None:
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
before = time.time()
|
|
collector.run("test")
|
|
after = time.time()
|
|
|
|
trace = store.list_traces()[0]
|
|
assert trace.started_at >= before
|
|
assert trace.ended_at <= after
|
|
assert trace.ended_at >= trace.started_at
|
|
store.close()
|
|
|
|
def test_unsubscribes_after_run(self, tmp_path: Path) -> None:
|
|
"""Events after run() completes should NOT affect the next trace."""
|
|
bus = EventBus()
|
|
store = TraceStore(tmp_path / "test.db")
|
|
agent = _FakeAgent(bus=bus)
|
|
collector = TraceCollector(agent, store=store, bus=bus)
|
|
|
|
collector.run("first")
|
|
|
|
# Emit events after run — should not affect stored trace
|
|
bus.publish(EventType.INFERENCE_START, {"model": "stray"})
|
|
bus.publish(EventType.INFERENCE_END, {"total_tokens": 999})
|
|
|
|
assert store.count() == 1
|
|
trace = store.list_traces()[0]
|
|
# No step with model="stray"
|
|
for s in trace.steps:
|
|
assert s.input.get("model") != "stray"
|
|
store.close()
|