Add coding-agent MCP bootstrap

This commit is contained in:
rob
2026-07-20 18:05:56 -03:00
parent 06a1606370
commit c289e812fb
8 changed files with 550 additions and 1 deletions
+179
View File
@@ -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")