"""Unit tests for project-relative path resolution in MCP server configs."""
from pathlib import Path
from navi.mcp.config import McpServerConfig, project_root, resolve_paths
class TestProjectRoot:
def test_is_the_directory_holding_the_config_dir(self, monkeypatch, tmp_path: Path):
monkeypatch.chdir(tmp_path)
assert project_root() == tmp_path
class TestResolvePaths:
def test_relative_command_and_cwd_become_absolute(self, tmp_path: Path):
cfg = McpServerConfig(
command="./mcp-servers/my_server/.venv/bin/python",
cwd="./mcp-servers/my_server",
)
resolved = resolve_paths(cfg, tmp_path)
assert resolved.command == str(tmp_path / "mcp-servers/my_server/.venv/bin/python")
assert resolved.cwd == str(tmp_path / "mcp-servers/my_server")
def test_a_relative_path_without_the_dot_slash_still_resolves(self, tmp_path: Path):
"""`command`/`cwd` are always paths — no marker needed there."""
cfg = McpServerConfig(command="mcp-servers/x/.venv/bin/python")
assert resolve_paths(cfg, tmp_path).command == str(
tmp_path / "mcp-servers/x/.venv/bin/python"
)
def test_absolute_paths_are_left_alone(self, tmp_path: Path):
cfg = McpServerConfig(
command="/usr/bin/python3",
cwd="/srv/somewhere",
env={"TOKEN_PATH": "/etc/navi/token"},
)
assert resolve_paths(cfg, tmp_path) == cfg
def test_relative_env_and_args_resolve_on_the_marker(self, tmp_path: Path):
cfg = McpServerConfig(
command="./bin/python",
args=["-m", "app.mcp_server", "--data", "./data"],
env={"SESSION_FILES_DIR": "./session_files", "MCP_TRANSPORT": "stdio"},
)
resolved = resolve_paths(cfg, tmp_path)
assert resolved.args == ["-m", "app.mcp_server", "--data", str(tmp_path / "data")]
assert resolved.env == {
"SESSION_FILES_DIR": str(tmp_path / "session_files"),
"MCP_TRANSPORT": "stdio",
}
def test_home_relative_paths_expand(self, tmp_path: Path):
cfg = McpServerConfig(command="~/bin/python")
assert resolve_paths(cfg, tmp_path).command == str(Path("~/bin/python").expanduser())
def test_env_values_that_are_not_paths_are_untouched(self, tmp_path: Path):
"""`env` carries URLs and flags too — the `./` marker is what opts in."""
cfg = McpServerConfig(
command="./bin/python",
env={"BASE_URL": "http://localhost:8000/mcp", "FLAG": "a/b"},
)
resolved = resolve_paths(cfg, tmp_path)
assert resolved.env == {"BASE_URL": "http://localhost:8000/mcp", "FLAG": "a/b"}
def test_http_transport_is_untouched(self, tmp_path: Path):
cfg = McpServerConfig(transport="sse", url="http://127.0.0.1:8098/mcp")
assert resolve_paths(cfg, tmp_path) == cfg
def test_does_not_mutate_the_original(self, tmp_path: Path):
"""`save_mcp_servers` writes configs back to the file: a resolved copy
leaking into it would bake this machine's paths into a tracked file."""
cfg = McpServerConfig(command="./mcp-servers/x/.venv/bin/python")
resolve_paths(cfg, tmp_path)
assert cfg.command == "./mcp-servers/x/.venv/bin/python"
def test_default_root_is_the_project_root(self, monkeypatch, tmp_path: Path):
monkeypatch.chdir(tmp_path)
cfg = McpServerConfig(cwd="./mcp-servers/x")
assert resolve_paths(cfg).cwd == str(tmp_path / "mcp-servers/x")