"""Tests for the ReMe CLI entry helpers.""" import asyncio from pathlib import Path import subprocess import sys from types import SimpleNamespace import pytest from reme.components.service import cli_service from reme.components.service.cli_service import CliService from reme import reme as reme_module from reme.components.base_component import ComponentMixin from reme.enumeration import ComponentEnum from reme.plugin import Backend, Plugin, PluginManager def _recording_client(seen, output="ok", base=object): """Build an async client stub that records construction and calls.""" class RecordingClient(base): """Async client stub backed by a shared call record.""" def __init__(self, **kwargs): seen["client_kwargs"] = kwargs async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): return None async def __call__(self, action: str, **kwargs): seen["action"] = action seen["payload"] = kwargs yield output return RecordingClient def _set_client_backend(monkeypatch, client_cls): monkeypatch.setattr( reme_module, "create_application_registry", lambda: SimpleNamespace(get=lambda component_type, backend: client_cls), ) def test_package_import_does_not_load_optional_core_dependencies(): """The base package leaves optional core dependencies unloaded.""" script = """ import importlib.abc class BlockOptionalCoreDependencies(importlib.abc.MetaPathFinder): def find_spec(self, fullname, path, target=None): blocked = ( "claude_agent_sdk", "dingtalk_stream", "openai_codex", "faiss", "jieba", "rjieba", "neo4j", "networkx", "pypdf", "polars", "tushare", ) if any(fullname == name or fullname.startswith(f"{name}.") for name in blocked): raise AssertionError(f"eagerly imported optional core dependency: {fullname}") return None import sys sys.meta_path.insert(0, BlockOptionalCoreDependencies()) import reme """ result = subprocess.run( [sys.executable, "-c", script], cwd=Path(__file__).resolve().parents[2], capture_output=True, text=True, check=False, ) assert result.returncode == 0, result.stderr def test_main_loads_env_before_calling_server(monkeypatch): """Client actions can resolve connection settings from the local .env.""" events = [] main_globals = reme_module.main.__globals__ monkeypatch.setitem(main_globals, "load_env", lambda: events.append("load_env")) monkeypatch.setitem(main_globals, "parse_args", lambda *_args: ("shell", {"cmd": "pwd"})) async def fake_call_server(action, **kwargs): events.append(("call_server", action, kwargs)) monkeypatch.setitem(main_globals, "call_server", fake_call_server) reme_module.main() assert events == ["load_env", ("call_server", "shell", {"cmd": "pwd"})] def test_main_saves_loaded_environment_in_start_config(monkeypatch): """The startup config keeps the environment captured by the single global load.""" observed = {} class FakeReMe: """Capture the fully resolved application configuration.""" def __init__(self, **kwargs): """Record the application startup configuration.""" observed["config"] = kwargs def run_app(self): """Record that application startup continued.""" observed["ran"] = True main_globals = reme_module.main.__globals__ monkeypatch.setitem(main_globals, "load_env", lambda: {"TOOL_ENV": "configured"}) monkeypatch.setitem(main_globals, "parse_args", lambda *_args: ("start", {})) monkeypatch.setitem(main_globals, "prepare_start_config", lambda _kwargs: {"service": {"backend": "cli"}}) monkeypatch.setitem(main_globals, "ReMe", FakeReMe) reme_module.main() assert observed == { "config": { "service": {"backend": "cli"}, "environment": {"TOOL_ENV": "configured"}, }, "ran": True, } def test_prepare_start_config_moves_unknown_start_args_to_job_args(monkeypatch): """``reme start job=...`` is translated into a one-shot cli service config.""" monkeypatch.setattr( cli_service, "resolve_app_config", lambda **kwargs: { **kwargs, "service": {"backend": "http", "host": "127.0.0.1"}, }, ) cfg = cli_service.prepare_start_config( { "config": "jinli_lme", "workspace_dir": "/tmp/reme", "job": "search", "query": "hello", "limit": 3, }, ) assert cfg["config"] == "jinli_lme" assert cfg["workspace_dir"] == "/tmp/reme" assert cfg["enable_logo"] is False assert cfg["log_to_console"] is False assert cfg["service"] == { "backend": "cli", "host": "127.0.0.1", "job": "search", "job_args": {"query": "hello", "limit": 3}, } def test_should_precheck_start_skips_cli_service(): """CLI service is local execution and should not run port prechecks.""" assert cli_service.should_precheck_start({"service": {"backend": "cli"}}) is False assert cli_service.should_precheck_start({"service": {"backend": "http"}}) is True def test_cli_service_runs_configured_job_and_closes_app(capsys): """CLI service runs one local job through app lifecycle and prints its answer.""" events = [] class FakeApp: """Minimal app stub for exercising CliService lifecycle.""" async def start(self): """Record app startup.""" events.append("start") async def close(self): """Record app shutdown.""" events.append("close") async def run_job(self, name, **kwargs): """Record job execution and return a successful response.""" events.append(("run_job", name, kwargs)) return SimpleNamespace(answer="found it", success=True, metadata={"hits": 1}) service = CliService(job="search", job_args={"query": "hello"}) service.start_service(FakeApp()) assert events == [ "start", ("run_job", "search", {"query": "hello"}), "close", ] assert capsys.readouterr().out == "found it\n" def test_cli_service_can_print_metadata_from_service_config(capsys): """service.show_metadata controls optional CLI metadata output.""" class FakeApp: """Minimal app stub for exercising metadata output.""" async def start(self): """No-op app startup.""" async def close(self): """No-op app shutdown.""" async def run_job(self, _name, **_kwargs): """Return a successful response with metadata.""" return SimpleNamespace(answer="found it", success=True, metadata={"hits": 1}) service = CliService(job="search", show_metadata=True) service.start_service(FakeApp()) assert capsys.readouterr().out == 'found it\n{"hits": 1}\n' def test_cli_service_exits_nonzero_on_failed_response(capsys): """Failed local CLI jobs write to stderr and produce a failing process status.""" events = [] class FakeApp: """Minimal app stub for exercising failure handling.""" async def start(self): """Record app startup.""" events.append("start") async def close(self): """Record app shutdown.""" events.append("close") async def run_job(self, name, **kwargs): """Record job execution and return a failed response.""" events.append(("run_job", name, kwargs)) return SimpleNamespace(answer="boom", success=False, metadata={}) service = CliService(job="search", job_args={"query": "hello"}) with pytest.raises(SystemExit) as exc_info: service.start_service(FakeApp()) assert exc_info.value.code == 1 assert events == [ "start", ("run_job", "search", {"query": "hello"}), "close", ] captured = capsys.readouterr() assert captured.out == "" assert captured.err == "boom\n" def test_call_server_passes_client_kwargs_to_client(monkeypatch, capsys): """CLI helper forwards connection options to the selected client.""" seen = {} _set_client_backend(monkeypatch, _recording_client(seen)) monkeypatch.setattr(reme_module, "running_app_config", lambda: None) async def run(): await reme_module.call_server( "search", backend="http", host="127.0.0.2", port=2444, timeout=1.5, query="hello", ) asyncio.run(run()) assert seen["client_kwargs"] == {"host": "127.0.0.2", "port": 2444, "timeout": 1.5} assert seen["action"] == "search" assert seen["payload"] == {"query": "hello"} assert capsys.readouterr().out == "ok\n" def test_call_server_treats_show_metadata_as_client_kwarg(monkeypatch, capsys): """show_metadata controls client display and is not sent as a tool argument.""" seen = {} _set_client_backend(monkeypatch, _recording_client(seen)) monkeypatch.setattr(reme_module, "running_app_config", lambda: None) async def run(): await reme_module.call_server("version", backend="http", show_metadata=True) asyncio.run(run()) assert seen["client_kwargs"] == {"show_metadata": True} assert seen["action"] == "version" assert seen["payload"] == {} assert capsys.readouterr().out == "ok\n" def test_call_server_passes_shell_parameters_as_payload(monkeypatch, capsys): """Shell-specific parameter names do not collide with client options.""" seen = {} _set_client_backend(monkeypatch, _recording_client(seen)) monkeypatch.setattr(reme_module, "running_app_config", lambda: None) async def run(): await reme_module.call_server("shell", backend="http", cmd="ls", shell_timeout=5) asyncio.run(run()) assert seen["action"] == "shell" assert seen["payload"] == {"cmd": "ls", "shell_timeout": 5} assert capsys.readouterr().out == "ok\n" def test_call_server_uses_running_plugins_and_their_service_defaults(monkeypatch, capsys): """A bare client call can load the Client backend enabled by the running app.""" seen = {} plugin_client = _recording_client(seen, output="plugin-ok", base=ComponentMixin) plugin_client.component_type = ComponentEnum.CLIENT manager = PluginManager( [ Plugin( name="example", backends=(Backend("plugin-client", plugin_client),), config={ "service": { "backend": "plugin-client", "host": "127.0.0.9", "port": 9911, }, }, ), ], ) monkeypatch.setattr( reme_module, "resolve_app_config", lambda **_kwargs: {"service": {"backend": "http"}}, ) monkeypatch.setattr(reme_module, "running_app_config", lambda: {"plugins": ["example"]}) monkeypatch.setattr(reme_module.PluginManager, "discover", lambda _specs: manager) asyncio.run(reme_module.call_server("search", query="hello")) assert seen["client_kwargs"]["host"] == "127.0.0.9" assert seen["client_kwargs"]["port"] == 9911 assert seen["action"] == "search" assert seen["payload"] == {"query": "hello"} assert capsys.readouterr().out == "plugin-ok\n" def test_call_server_skips_local_fallback_when_server_is_running(monkeypatch, capsys): """A usable running config prevents eager parsing of the local fallback.""" seen = {} _set_client_backend(monkeypatch, _recording_client(seen)) monkeypatch.setattr(reme_module, "running_app_config", lambda: {"service": {"backend": "http"}}) def fail_local_resolution(**_kwargs): raise AssertionError("local fallback should not be resolved") monkeypatch.setattr(reme_module, "resolve_app_config", fail_local_resolution) asyncio.run(reme_module.call_server("version")) assert seen["action"] == "version" assert seen["payload"] == {} assert capsys.readouterr().out == "ok\n"