Add coding-agent MCP bootstrap
This commit is contained in:
@@ -34,6 +34,28 @@ class TestCLIBasics:
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
def test_mcp_configure_dispatches_host_options(self):
|
||||
with (
|
||||
patch(
|
||||
"cmdforge.agent_setup.configure_agent_host",
|
||||
return_value=0,
|
||||
) as configure,
|
||||
patch("sys.argv", [
|
||||
"cmdforge", "mcp", "configure", "claude-code",
|
||||
"--scope", "project", "--dry-run",
|
||||
]),
|
||||
):
|
||||
assert main() == 0
|
||||
|
||||
configure.assert_called_once_with(
|
||||
"claude-code",
|
||||
server_name="cmdforge",
|
||||
scope="project",
|
||||
install_policy=True,
|
||||
dry_run=True,
|
||||
force=False,
|
||||
)
|
||||
|
||||
|
||||
class TestListCommand:
|
||||
"""Tests for 'cmdforge list' command."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for MCP client and McpStep execution."""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,6 +24,184 @@ from cmdforge.runner import _substitute_mcp_args, _deep_substitute, run_tool
|
||||
from cmdforge.tool import McpStep, Tool
|
||||
|
||||
|
||||
class TestAgentHostSetup:
|
||||
def test_builds_current_vendor_cli_commands(self):
|
||||
from cmdforge.agent_setup import build_host_add_command
|
||||
|
||||
codex = build_host_add_command(
|
||||
"codex", "/usr/bin/codex", "cmdforge", "user"
|
||||
)
|
||||
assert codex[:5] == [
|
||||
"/usr/bin/codex", "mcp", "add", "cmdforge", "--"
|
||||
]
|
||||
assert codex[-4:] == ["-m", "cmdforge.cli", "mcp", "serve"]
|
||||
|
||||
claude = build_host_add_command(
|
||||
"claude-code", "/usr/bin/claude", "cmdforge", "project"
|
||||
)
|
||||
assert claude[:9] == [
|
||||
"/usr/bin/claude", "mcp", "add", "--transport", "stdio",
|
||||
"--scope", "project", "cmdforge", "--",
|
||||
]
|
||||
|
||||
def test_codex_rejects_unsupported_scope(self):
|
||||
from cmdforge.agent_setup import build_host_add_command
|
||||
|
||||
with pytest.raises(ValueError, match="user scope only"):
|
||||
build_host_add_command(
|
||||
"codex", "/usr/bin/codex", "cmdforge", "project"
|
||||
)
|
||||
|
||||
def test_rejects_unsafe_registration_name(self):
|
||||
from cmdforge.agent_setup import build_host_add_command
|
||||
|
||||
with pytest.raises(ValueError, match="registration name"):
|
||||
build_host_add_command(
|
||||
"codex", "/usr/bin/codex", "--config", "user"
|
||||
)
|
||||
|
||||
def test_policy_merge_is_idempotent_and_preserves_user_text(self):
|
||||
from cmdforge.agent_setup import POLICY_BODY, merge_agent_policy
|
||||
|
||||
original = "# Existing guidance\n\nKeep this.\n"
|
||||
merged = merge_agent_policy(original)
|
||||
assert merged.startswith(original)
|
||||
assert POLICY_BODY.strip() in merged
|
||||
assert merge_agent_policy(merged) == merged
|
||||
|
||||
def test_policy_merge_rejects_malformed_markers(self):
|
||||
from cmdforge.agent_setup import POLICY_START, merge_agent_policy
|
||||
|
||||
with pytest.raises(ValueError, match="malformed"):
|
||||
merge_agent_policy(f"text\n{POLICY_START}\n")
|
||||
|
||||
def test_policy_writer_refuses_symlink(self, tmp_path):
|
||||
from cmdforge.agent_setup import write_agent_policy
|
||||
|
||||
target = tmp_path / "target.md"
|
||||
target.write_text("unchanged")
|
||||
link = tmp_path / "AGENTS.md"
|
||||
link.symlink_to(target)
|
||||
with pytest.raises(ValueError, match="symlinked"):
|
||||
write_agent_policy(link, "replacement")
|
||||
assert target.read_text() == "unchanged"
|
||||
|
||||
def test_dry_run_changes_nothing(self, tmp_path, capsys):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("cmdforge.agent_setup.subprocess.run") as run,
|
||||
):
|
||||
assert configure_agent_host(
|
||||
"codex", project_dir=tmp_path, dry_run=True
|
||||
) == 0
|
||||
run.assert_not_called()
|
||||
assert not (tmp_path / "AGENTS.md").exists()
|
||||
output = capsys.readouterr().out
|
||||
assert "Would run:" in output
|
||||
assert "cmdforge-agent-policy:start" in output
|
||||
|
||||
def test_configures_host_and_managed_policy(self, tmp_path, capsys):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
results = [
|
||||
subprocess.CompletedProcess([], 1, "", "not found"),
|
||||
subprocess.CompletedProcess([], 0, "added", ""),
|
||||
]
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("cmdforge.agent_setup.subprocess.run", side_effect=results) as run,
|
||||
patch("cmdforge.agent_setup._warn_if_no_tools_exposed"),
|
||||
):
|
||||
assert configure_agent_host("codex", project_dir=tmp_path) == 0
|
||||
|
||||
assert run.call_count == 2
|
||||
assert run.call_args_list[0].args[0] == [
|
||||
"/usr/bin/codex", "mcp", "get", "cmdforge"
|
||||
]
|
||||
assert run.call_args_list[1].args[0][:5] == [
|
||||
"/usr/bin/codex", "mcp", "add", "cmdforge", "--"
|
||||
]
|
||||
policy = (tmp_path / "AGENTS.md").read_text()
|
||||
assert "cmdforge-agent-policy:start" in policy
|
||||
assert "Do not install, publish, expose" in policy
|
||||
assert "Configured CmdForge MCP" in capsys.readouterr().out
|
||||
|
||||
def test_existing_registration_is_not_overwritten(self, tmp_path):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
result = subprocess.CompletedProcess([], 0, "exists", "")
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/claude"),
|
||||
patch("cmdforge.agent_setup.subprocess.run", return_value=result) as run,
|
||||
patch("cmdforge.agent_setup._warn_if_no_tools_exposed"),
|
||||
):
|
||||
assert configure_agent_host(
|
||||
"claude-code", project_dir=tmp_path
|
||||
) == 0
|
||||
assert run.call_count == 1
|
||||
assert (tmp_path / "CLAUDE.md").exists()
|
||||
|
||||
def test_force_removes_then_readds_registration(self, tmp_path):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
results = [
|
||||
subprocess.CompletedProcess([], 0, "exists", ""),
|
||||
subprocess.CompletedProcess([], 0, "removed", ""),
|
||||
subprocess.CompletedProcess([], 0, "added", ""),
|
||||
]
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/claude"),
|
||||
patch("cmdforge.agent_setup.subprocess.run", side_effect=results) as run,
|
||||
patch("cmdforge.agent_setup._warn_if_no_tools_exposed"),
|
||||
):
|
||||
assert configure_agent_host(
|
||||
"claude-code", project_dir=tmp_path,
|
||||
install_policy=False, force=True,
|
||||
) == 0
|
||||
|
||||
assert run.call_args_list[1].args[0] == [
|
||||
"/usr/bin/claude", "mcp", "remove", "cmdforge"
|
||||
]
|
||||
assert run.call_args_list[2].args[0][:4] == [
|
||||
"/usr/bin/claude", "mcp", "add", "--transport"
|
||||
]
|
||||
|
||||
def test_failed_host_add_does_not_write_policy(self, tmp_path):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
results = [
|
||||
subprocess.CompletedProcess([], 1, "", "missing"),
|
||||
subprocess.CompletedProcess([], 1, "", "bad config"),
|
||||
]
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("cmdforge.agent_setup.subprocess.run", side_effect=results),
|
||||
):
|
||||
assert configure_agent_host(
|
||||
"codex", project_dir=tmp_path
|
||||
) == 1
|
||||
|
||||
assert not (tmp_path / "AGENTS.md").exists()
|
||||
|
||||
def test_host_command_timeout_is_reported(self, tmp_path, capsys):
|
||||
from cmdforge.agent_setup import configure_agent_host
|
||||
|
||||
with (
|
||||
patch("cmdforge.agent_setup.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch(
|
||||
"cmdforge.agent_setup.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired("codex", 30),
|
||||
),
|
||||
):
|
||||
assert configure_agent_host(
|
||||
"codex", project_dir=tmp_path, install_policy=False
|
||||
) == 1
|
||||
|
||||
assert "timed out after 30 seconds" in capsys.readouterr().err
|
||||
|
||||
|
||||
class TestMcpServerConfig:
|
||||
def test_defaults(self):
|
||||
cfg = McpServerConfig(name="test")
|
||||
|
||||
Reference in New Issue
Block a user