"""Native MCP settings, endpoint isolation, and MCP-only hook transport.""" # pylint: disable=missing-function-docstring,redefined-outer-name,protected-access import asyncio import importlib import json from datetime import datetime, timezone from pathlib import Path from unittest.mock import AsyncMock import pytest from fastmcp import Client, FastMCP from fastmcp.exceptions import ToolError from mcp.shared.exceptions import McpError PLUGIN = Path(__file__).resolve().parents[2] / "integrations/codex/plugins/reme" @pytest.fixture def plugin(tmp_path, monkeypatch): monkeypatch.syspath_prepend(str(PLUGIN)) monkeypatch.setenv("CODEX_HOME", str(tmp_path / "host")) config = importlib.import_module("reme_config") runtime = importlib.import_module("reme_runtime") # Protocol tests must never start a scheduled network call at the wall-clock boundary. monkeypatch.setattr(runtime, "next_daily_run", lambda *_: datetime(2099, 1, 1, tzinfo=timezone.utc)) server = importlib.import_module("mcp_server") transport = importlib.import_module("reme_mcp") return config, server, transport @pytest.fixture def upstreams(plugin, monkeypatch): config, server, _ = plugin calls = [] servers = {} for name in ("first", "second"): upstream = FastMCP(name) def search(query: str, _name=name) -> str: calls.append((_name, query)) return f"{_name}: daily/fact.md" upstream.tool(search) servers[f"http://{name}.test/mcp"] = upstream def client(url, **kwargs): return Client(servers[url], **kwargs) monkeypatch.setattr(server, "ProxyClient", client) config.update_settings({"mcpUrl": "http://first.test/mcp"}) return calls @pytest.mark.asyncio async def test_settings_remain_editable_offline(plugin, monkeypatch): config, module, _ = plugin connect = AsyncMock(side_effect=ConnectionError("offline")) monkeypatch.setattr(module.ReMeProvider, "_list_tools", connect) async with Client(module.create_server()) as client: capability = client.initialize_result.capabilities.experimental["openai/settings"] assert capability == {"readTool": module.SETTINGS_READ, "updateTool": module.SETTINGS_UPDATE} tools = {tool.name: tool for tool in await client.list_tools()} assert {module.SETTINGS_READ, module.SETTINGS_UPDATE, module.STATUS} <= tools.keys() assert "reme_check_connection" not in tools read = await client.call_tool(capability["readTool"], {}) settings = read.structured_content assert settings["values"] == config.DEFAULTS assert settings["schema"]["properties"].keys() == config.DEFAULTS.keys() assert settings["layout"][0]["items"][1]["tool"] == module.STATUS assert len(settings["layout"][0]["items"]) == 2 assert tools[module.SETTINGS_READ].annotations.readOnlyHint assert tools[module.SETTINGS_READ].outputSchema assert not (config.data_dir() / "config.json").exists() saved = await client.call_tool(capability["updateTool"], {"set": {"autoMemoryEnabled": False}}) assert saved.structured_content["values"] == {**config.DEFAULTS, "autoMemoryEnabled": False} assert not config.load_config()["autoMemoryEnabled"] @pytest.mark.asyncio async def test_settings_patch_validation_and_persistence(plugin, upstreams): config, module, _ = plugin path = config.data_dir() / "config.json" config.write_json(path, {"mcpUrl": "http://first.test/mcp", "searchLimit": 9}) before = path.read_bytes() async with Client(module.create_server()) as client: for changes in ( {}, {"bogus": 1}, {"autoMemoryEnabled": "false"}, {"autoMemoryInterval": 0}, {"mcpUrl": "http://user:secret@first.test/mcp"}, {"timezone": "Invalid/Zone"}, {"backgroundTimeoutMs": 0}, {"language": "fr"}, {"searchLimit": 51}, {"autoMemoryInterval": 1001}, {"api_url": "http://first.test"}, ): with pytest.raises(ToolError): await client.call_tool(module.SETTINGS_UPDATE, {"set": changes}) assert path.read_bytes() == before await client.call_tool(module.SETTINGS_UPDATE, {"set": {"autoRecall": False}}) assert config.load_config()["searchLimit"] == 9 assert config.load_config()["endpoint"] == "http://first.test" assert "api_url" not in path.read_text() assert path.stat().st_mode & 0o777 == 0o600 with config.lock(config.data_dir() / "config.lock"): with pytest.raises(RuntimeError, match="in progress"): config.update_settings({"autoRecall": True}) assert not config.load_config()["autoRecall"] assert not upstreams @pytest.mark.asyncio async def test_live_endpoint_switch_and_inflight_snapshot(plugin, upstreams): config, module, _ = plugin server = module.create_server() provider = module.ReMeProvider() previous = await provider.get_tool("search") @server.tool async def captured_search(query: str): return await previous.run({"query": query}) async with Client(server) as client: assert (await client.call_tool("search", {"query": "one"})).data == "first: daily/fact.md" await client.call_tool(module.SETTINGS_UPDATE, {"set": {"mcpUrl": "http://second.test/mcp"}}) assert (await client.call_tool("search", {"query": "two"})).data == "second: daily/fact.md" # A resolved call retains its original endpoint even when settings change before execution. await client.call_tool("captured_search", {"query": "in flight"}) assert upstreams == [("first", "one"), ("second", "two"), ("first", "in flight")] assert config.load_config()["mcpUrl"] == "http://second.test/mcp" def test_custom_endpoint_does_not_alias_standard_queue(plugin): config, _, _ = plugin standard = config.validate_config({"mcpUrl": "http://service.test/mcp"}) custom = config.validate_config({"mcpUrl": "http://service.test"}) assert standard["endpoint"] == "http://service.test" assert custom["endpoint"] != standard["endpoint"] @pytest.mark.asyncio async def test_hooks_call_mcp_and_preserve_structured_answers(plugin, monkeypatch): config, _, transport = plugin upstream = FastMCP("test") calls = [] @upstream.tool def search(query: str) -> dict: calls.append(query) return {"path": "daily/fact.md"} @upstream.tool def auto_memory() -> str: raise ToolError("write failed") @upstream.tool async def delayed() -> str: await asyncio.sleep(1) return "too late" monkeypatch.setattr("fastmcp.Client", lambda *args, **kwargs: Client(upstream, **kwargs)) result = await transport.call_async(config.load_config(), "search", {"query": "fact"}, 2) assert result == {"success": True, "answer": {"path": "daily/fact.md"}} assert calls == ["fact"] with pytest.raises(ToolError, match="write failed"): await transport.call_async(config.load_config(), "auto_memory", {}, 2) with pytest.raises((TimeoutError, McpError)): await transport.call_async(config.load_config(), "delayed", {}, 0.05) @pytest.mark.asyncio async def test_status_checks_current_connection_and_reports_failure_without_hiding_local_state( plugin, upstreams, monkeypatch, ): config, module, _ = plugin call = AsyncMock(return_value={"success": True, "answer": "healthy"}) monkeypatch.setattr(importlib.import_module("reme_tools"), "call_async", call) async with Client(module.create_server()) as client: await client.call_tool( module.SETTINGS_UPDATE, {"set": {"mcpUrl": "http://second.test/mcp", "requestTimeoutMs": 3000}}, ) result = await client.call_tool(module.STATUS, {}) assert result.structured_content["service"]["health_check"] == {"reachable": True, "answer": "healthy"} assert {args.args[1] for args in call.call_args_list} == {"health_check", "status"} assert all(args.args[0] == config.load_config() and args.args[2:] == ({}, 3) for args in call.call_args_list) assert result.structured_content["checked_at"] > 0 assert result.structured_content["language"] == "en" call.side_effect = ConnectionError("offline") offline = (await client.call_tool(module.STATUS, {})).structured_content assert offline["service"]["health_check"] == {"reachable": False, "error": "ConnectionError"} assert offline["auto_memory"]["queued_turns"] == 0 assert not upstreams @pytest.mark.asyncio async def test_status_opens_a_self_contained_fullscreen_app_resource(plugin): _, module, _ = plugin async with Client(module.create_server()) as client: tools = {tool.name: tool for tool in await client.list_tools()} ui = tools["reme_status"].meta["ui"] assert ui["visibility"] == ["app", "model"] content = (await client.read_resource(ui["resourceUri"]))[0] assert content.mimeType == "text/html;profile=mcp-app" assert content.meta["openai/ui"] == { "preferredDisplayMode": "fullscreen", "availableDisplayModes": ["fullscreen"], } assert content.meta["ui"]["csp"] == {"connectDomains": [], "resourceDomains": []} assert 'id="refresh"' in content.text assert "ui/initialize" in content.text assert "ui/notifications/tool-result" in content.text assert "/* STATUS_APP */" not in content.text assert "