feat: config-driven architecture, install wizard, live runtime switching, usage tracking, auto-failover
Major changes: - Config-driven adapters: all channels (Slack, Discord, Telegram, WebChat, Webhooks) controlled via config.json with enabled flags and token auto-detection, no CLI flags required - Runtime engine field: runtime.engine selects opencode/claude from config - Interactive install script: 8-phase setup wizard with AI runtime detection/installation, token setup, identity file personalization (personality presets), aetheel CLI command, background service (launchd/systemd) - Live runtime switching: /engine, /model, /provider commands hot-swap the AI runtime from chat without restart, changes persisted to config.json - Usage tracking: per-request cost extraction from Claude Code JSON output, cumulative stats via /usage command - Auto-failover: rate limit detection on both runtimes, automatic switch to other engine on quota errors with user notification - Chat commands work without / prefix (Slack intercepts / in channels), commands: engine, model, provider, config, usage, reload, cron, subagents, status, help - /config set for editing config.json from chat with dotted key notation - Security audit saved to docs/security-audit.md - Full command reference in docs/commands.md - Future changes doc with NanoClaw agent teams analysis - Logo added to README and WebChat UI - README fully rewritten with all features documented
This commit is contained in:
134
tests/test_mcp_config.py
Normal file
134
tests/test_mcp_config.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Tests for the MCP config writer utility (write_mcp_config).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from config import MCPConfig, MCPServerConfig, write_mcp_config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(tmp_path):
|
||||
"""Return a temporary workspace directory path."""
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_mcp_config():
|
||||
"""Return an MCPConfig with one server entry."""
|
||||
return MCPConfig(
|
||||
servers={
|
||||
"my-tool": MCPServerConfig(
|
||||
command="npx",
|
||||
args=["-y", "@my/tool"],
|
||||
env={"API_KEY": "test123"},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: write_mcp_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWriteMcpConfig:
|
||||
def test_skips_when_no_servers(self, workspace):
|
||||
"""Empty servers dict should not create any file."""
|
||||
write_mcp_config(MCPConfig(), workspace, use_claude=True)
|
||||
assert not os.path.exists(os.path.join(workspace, ".mcp.json"))
|
||||
assert not os.path.exists(os.path.join(workspace, "opencode.json"))
|
||||
|
||||
def test_writes_mcp_json_for_claude(self, workspace, sample_mcp_config):
|
||||
"""Claude runtime should produce .mcp.json with 'mcpServers' key."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=True)
|
||||
path = os.path.join(workspace, ".mcp.json")
|
||||
assert os.path.isfile(path)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "mcpServers" in data
|
||||
assert "my-tool" in data["mcpServers"]
|
||||
srv = data["mcpServers"]["my-tool"]
|
||||
assert srv["command"] == "npx"
|
||||
assert srv["args"] == ["-y", "@my/tool"]
|
||||
assert srv["env"] == {"API_KEY": "test123"}
|
||||
|
||||
def test_writes_opencode_json_for_opencode(self, workspace, sample_mcp_config):
|
||||
"""OpenCode runtime should produce opencode.json with 'mcp' key."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=False)
|
||||
path = os.path.join(workspace, "opencode.json")
|
||||
assert os.path.isfile(path)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "mcp" in data
|
||||
assert "my-tool" in data["mcp"]
|
||||
|
||||
def test_multiple_servers(self, workspace):
|
||||
"""Multiple server entries should all appear in the output."""
|
||||
cfg = MCPConfig(
|
||||
servers={
|
||||
"server-a": MCPServerConfig(command="cmd-a", args=["--flag"]),
|
||||
"server-b": MCPServerConfig(command="cmd-b", env={"X": "1"}),
|
||||
}
|
||||
)
|
||||
write_mcp_config(cfg, workspace, use_claude=True)
|
||||
|
||||
with open(os.path.join(workspace, ".mcp.json"), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert len(data["mcpServers"]) == 2
|
||||
assert "server-a" in data["mcpServers"]
|
||||
assert "server-b" in data["mcpServers"]
|
||||
|
||||
def test_creates_parent_directories(self, tmp_path):
|
||||
"""Should create intermediate directories if workspace doesn't exist."""
|
||||
nested = str(tmp_path / "deep" / "nested" / "workspace")
|
||||
cfg = MCPConfig(
|
||||
servers={"s": MCPServerConfig(command="echo")}
|
||||
)
|
||||
write_mcp_config(cfg, nested, use_claude=True)
|
||||
assert os.path.isfile(os.path.join(nested, ".mcp.json"))
|
||||
|
||||
def test_produces_valid_json(self, workspace, sample_mcp_config):
|
||||
"""Output file must be valid, parseable JSON."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=True)
|
||||
path = os.path.join(workspace, ".mcp.json")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f) # Would raise on invalid JSON
|
||||
assert isinstance(data, dict)
|
||||
|
||||
def test_overwrites_existing_file(self, workspace):
|
||||
"""Writing twice should overwrite the previous config."""
|
||||
cfg1 = MCPConfig(servers={"old": MCPServerConfig(command="old-cmd")})
|
||||
cfg2 = MCPConfig(servers={"new": MCPServerConfig(command="new-cmd")})
|
||||
|
||||
write_mcp_config(cfg1, workspace, use_claude=True)
|
||||
write_mcp_config(cfg2, workspace, use_claude=True)
|
||||
|
||||
with open(os.path.join(workspace, ".mcp.json"), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "new" in data["mcpServers"]
|
||||
assert "old" not in data["mcpServers"]
|
||||
|
||||
def test_tilde_expansion(self, monkeypatch, tmp_path):
|
||||
"""Workspace path with ~ should be expanded."""
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
# Also handle Windows
|
||||
monkeypatch.setattr(os.path, "expanduser", lambda p: p.replace("~", str(tmp_path)))
|
||||
|
||||
cfg = MCPConfig(servers={"s": MCPServerConfig(command="echo")})
|
||||
write_mcp_config(cfg, "~/my-workspace", use_claude=False)
|
||||
assert os.path.isfile(os.path.join(str(tmp_path), "my-workspace", "opencode.json"))
|
||||
Reference in New Issue
Block a user