mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
214 lines
7.4 KiB
Python
214 lines
7.4 KiB
Python
"""Regression tests for #263 — ``jarvis serve`` must build the system once.
|
|
|
|
serve.py used to construct all heavy components inline and then call
|
|
``SystemBuilder(config).build()`` a second time inside the scheduler block,
|
|
re-discovering the engine, re-instrumenting it, re-resolving tools, re-opening
|
|
the channel and re-creating the agent manager — ~30-40s of redundant work.
|
|
|
|
These tests pin the fix:
|
|
|
|
1. ``SystemBuilder.build`` is never called during ``jarvis serve`` startup
|
|
(the duplicate build is gone).
|
|
2. The ``AgentExecutor`` still receives a system exposing the attributes it
|
|
actually reads: ``tool_executor``, ``session_store``, ``memory_backend``,
|
|
plus ``engine`` / ``model`` / ``config``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from openjarvis.cli import cli
|
|
|
|
pytest.importorskip("fastapi")
|
|
pytest.importorskip("uvicorn")
|
|
|
|
# ``openjarvis.cli.serve`` as an attribute resolves to the click *command*
|
|
# (re-exported on the package); grab the real module to monkeypatch its globals.
|
|
serve_mod = importlib.import_module("openjarvis.cli.serve")
|
|
|
|
|
|
def _fake_engine() -> MagicMock:
|
|
engine = MagicMock()
|
|
engine.list_models.return_value = ["test-model"]
|
|
engine.health.return_value = True
|
|
engine.name = "mock"
|
|
return engine
|
|
|
|
|
|
def _repopulate_registries() -> None:
|
|
"""Re-run the @register decorators wiped by the autouse conftest fixture.
|
|
|
|
The tool/memory modules are import-cached, so a plain ``import`` inside
|
|
serve.py is a no-op after the registries are cleared per-test. Reload the
|
|
individual submodules so ToolRegistry/MemoryRegistry are populated exactly
|
|
as they would be on a fresh process — otherwise serve would resolve an
|
|
empty tool list and no memory backend, masking the very wiring under test.
|
|
"""
|
|
import importlib
|
|
import sys
|
|
|
|
import openjarvis.agents # noqa: F401
|
|
import openjarvis.tools # noqa: F401
|
|
import openjarvis.tools.storage # noqa: F401
|
|
from openjarvis.core.registry import (
|
|
AgentRegistry,
|
|
MemoryRegistry,
|
|
ToolRegistry,
|
|
)
|
|
|
|
if not AgentRegistry.keys():
|
|
for mod_name in list(sys.modules):
|
|
if mod_name.startswith("openjarvis.agents.") and not mod_name.endswith(
|
|
"_stubs"
|
|
):
|
|
try:
|
|
importlib.reload(sys.modules[mod_name])
|
|
except Exception:
|
|
pass
|
|
|
|
if not ToolRegistry.keys():
|
|
for mod_name in list(sys.modules):
|
|
if (
|
|
mod_name.startswith("openjarvis.tools.")
|
|
and not mod_name.endswith("_stubs")
|
|
and not mod_name.endswith("agent_tools")
|
|
):
|
|
try:
|
|
importlib.reload(sys.modules[mod_name])
|
|
except Exception:
|
|
pass
|
|
|
|
if not MemoryRegistry.keys():
|
|
for mod_name in list(sys.modules):
|
|
if mod_name.startswith(
|
|
"openjarvis.tools.storage."
|
|
) and not mod_name.endswith("_stubs"):
|
|
try:
|
|
importlib.reload(sys.modules[mod_name])
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _run_serve(tmp_path, monkeypatch, *, build_spy, set_system_spy):
|
|
"""Invoke ``jarvis serve`` with all heavy/blocking pieces stubbed out.
|
|
|
|
Returns the CliRunner result. The server is never actually started
|
|
(``uvicorn.run`` is a no-op) and no real engine is contacted.
|
|
"""
|
|
from openjarvis.core.config import JarvisConfig
|
|
|
|
_repopulate_registries()
|
|
|
|
config = JarvisConfig()
|
|
# Keep the scheduler block alive (it owns the executor wiring under test)
|
|
# while pointing every store at the temp dir.
|
|
config.agent_manager.enabled = True
|
|
config.agent_manager.db_path = str(tmp_path / "agents.db")
|
|
config.sessions.enabled = True
|
|
config.sessions.db_path = str(tmp_path / "sessions.db")
|
|
config.memory.db_path = str(tmp_path / "memory.db")
|
|
config.telemetry.enabled = False
|
|
config.traces.enabled = False
|
|
config.channel.enabled = False
|
|
config.skills.enabled = False
|
|
config.server.host = "127.0.0.1"
|
|
config.server.port = 8123
|
|
# Resolve a model without contacting a real engine / discovery.
|
|
config.intelligence.default_model = "test-model"
|
|
|
|
engine = _fake_engine()
|
|
|
|
monkeypatch.setattr(serve_mod, "load_config", lambda *a, **k: config)
|
|
monkeypatch.setattr(serve_mod, "get_engine", lambda *a, **k: ("mock", engine))
|
|
monkeypatch.setattr(serve_mod, "discover_engines", lambda *a, **k: {})
|
|
monkeypatch.setattr(serve_mod, "discover_models", lambda *a, **k: {})
|
|
|
|
# setup_security returns its own context; pass the engine straight through
|
|
# so we don't need real guardrails wired up.
|
|
sec = MagicMock()
|
|
sec.engine = engine
|
|
sec.capability_policy = None
|
|
sec.audit_logger = None
|
|
monkeypatch.setattr("openjarvis.security.setup_security", lambda *a, **k: sec)
|
|
|
|
with (
|
|
patch(
|
|
"openjarvis.system.builder.SystemBuilder.build",
|
|
build_spy,
|
|
),
|
|
patch(
|
|
"openjarvis.agents.executor.AgentExecutor.set_system",
|
|
set_system_spy,
|
|
),
|
|
patch("uvicorn.run", lambda *a, **k: None),
|
|
):
|
|
return CliRunner().invoke(cli, ["serve"], catch_exceptions=False)
|
|
|
|
|
|
def test_serve_does_not_call_systembuilder_build(tmp_path, monkeypatch):
|
|
"""The redundant second full build is gone (#263)."""
|
|
build_spy = MagicMock(
|
|
side_effect=AssertionError(
|
|
"SystemBuilder.build() must not run during `jarvis serve` startup "
|
|
"— it is the duplicate build #263 removed."
|
|
)
|
|
)
|
|
set_system_spy = MagicMock()
|
|
inject_spy = MagicMock()
|
|
monkeypatch.setattr(serve_mod, "inject_credentials", inject_spy)
|
|
|
|
result = _run_serve(
|
|
tmp_path,
|
|
monkeypatch,
|
|
build_spy=build_spy,
|
|
set_system_spy=set_system_spy,
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
build_spy.assert_not_called()
|
|
inject_spy.assert_called_once_with()
|
|
|
|
|
|
def test_executor_receives_required_system_attrs(tmp_path, monkeypatch):
|
|
"""The executor still gets a system exposing the attributes it reads.
|
|
|
|
AgentExecutor reads engine/model/config/memory_backend/tool_executor/
|
|
session_store off ``self._system``; the de-dup must not strip any of them.
|
|
"""
|
|
build_spy = MagicMock()
|
|
captured: dict = {}
|
|
|
|
def _capture_set_system(self, system): # noqa: ANN001
|
|
captured["system"] = system
|
|
# Preserve real behaviour so the executor is usable afterwards.
|
|
self._system = system
|
|
|
|
result = _run_serve(
|
|
tmp_path,
|
|
monkeypatch,
|
|
build_spy=build_spy,
|
|
set_system_spy=_capture_set_system,
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
# Built once, from the inline components — not via SystemBuilder.build().
|
|
build_spy.assert_not_called()
|
|
|
|
system = captured.get("system")
|
|
assert system is not None, "executor.set_system was never called"
|
|
|
|
# Correctness constraint from the verifier: these must survive the de-dup.
|
|
assert system.tool_executor is not None
|
|
assert system.session_store is not None
|
|
assert system.memory_backend is not None
|
|
|
|
# And the basics the executor resolves engine/model from.
|
|
assert system.engine is not None
|
|
assert system.model == "test-model"
|
|
assert system.config is not None
|