Files
roboco/tests/unit/runtime/test_spawn_strict_mcp.py
0bf0cd69b3 fix(release): close the 0.19.0 scan findings — sandbox mongo tag, flow-verb timeout walls, video hardening (#329)
- mongo:8-alpine → mongo:8 (tag never existed; a mongo-opted project could spawn no agents) + a Docker Hub tag-existence e2e guard for every sandbox engine
- flow-verb timeouts at both walls: shared SLOW_VERBS policy (i_am_done / submit_up / submit_root / open_pr / i_will_work_on get the 900s server budget); the MCP client now outlasts the server budget (+10s headroom, orchestrator-injected env) so agents receive the middleware's clean 504 envelope instead of dying at the old flat 30s client timeout
- cancellation safety: the quality gate kills+reaps its child on CancelledError; create_pr records the PR via a shield-with-wait-out helper so the write can neither be skipped nor race get_db's rollback
- video engine: renderer sidecar isolated on a render-only network, 2g/2cpu caps, 570s render watchdog with exit-on-hang, 512MB tar decompression cap, CEO notification on terminal render failure, reject under the approve mutex (fail-closed on Redis-down)
- dead python-jose dependency removed (drops ecdsa and its unfixable Minerva advisory PYSEC-2026-1325); panel --font-mono now a real monospace stack

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 03:26:12 +02:00

122 lines
5.4 KiB
Python

"""Wave E3: agent spawn cmd uses --strict-mcp-config to suppress builtin
Anthropic connectors (Gmail / Google Calendar / Notion / Google Drive).
Without --strict-mcp-config, the Claude Code CLI auto-registers its
builtin MCP connectors alongside our --mcp-config entries — they appear
in the agent's tool inventory as `mcp__claude_ai_Gmail__authenticate`
etc. The flag tells the CLI to load ONLY the servers from --mcp-config.
"""
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import patch
import pytest
from roboco.config import settings
from roboco.models.runtime import OrchestratorAgentConfig, SpawnGitContext
from roboco.runtime.orchestrator import AgentOrchestrator
_MAX_FLAG_ADJACENCY = 3
def _make_dev_config() -> OrchestratorAgentConfig:
"""Minimal AgentConfig for a developer."""
return OrchestratorAgentConfig(
agent_id="be-dev-1",
blueprint_path=Path("/app/agents/blueprints/be-dev-1.md"),
model="sonnet",
mcp_config_path=Path("/app/mcp-config.json"),
git_context=SpawnGitContext(
project_slug="roboco-api",
branch_name="feature/backend/TASK0001",
),
)
def _build_image_args(config: OrchestratorAgentConfig) -> list[str]:
"""Invoke _append_image_and_claude_args against an empty cmd."""
cmd: list[str] = []
with patch(
"roboco.runtime.orchestrator._resolve_agent_cli_model",
return_value="claude-sonnet-5",
):
AgentOrchestrator._append_image_and_claude_args(cmd, config, None)
return cmd
class TestSpawnStrictMcpConfig:
"""Agent spawn cmd includes --strict-mcp-config."""
def test_cmd_contains_strict_mcp_config_flag(self) -> None:
"""docker run cmd for an agent includes --strict-mcp-config."""
cmd = _build_image_args(_make_dev_config())
assert "--strict-mcp-config" in cmd, (
f"--strict-mcp-config flag missing from spawn cmd. Without it, "
f"the Claude CLI auto-registers builtin connectors (Gmail, "
f"Calendar, Notion, Drive) alongside our roboco MCP servers. "
f"Full cmd: {cmd}"
)
def test_strict_mcp_config_paired_with_mcp_config(self) -> None:
"""--strict-mcp-config appears alongside --mcp-config /app/mcp-config.json."""
cmd = _build_image_args(_make_dev_config())
assert "--mcp-config" in cmd
mcp_idx = cmd.index("--mcp-config")
assert cmd[mcp_idx + 1] == "/app/mcp-config.json"
strict_idx = cmd.index("--strict-mcp-config")
assert abs(strict_idx - mcp_idx) <= _MAX_FLAG_ADJACENCY, (
f"--strict-mcp-config should appear near --mcp-config; "
f"strict_idx={strict_idx}, mcp_idx={mcp_idx}. Cmd: {cmd}"
)
class TestMcpConfigPinsBakedVenv:
"""#179: every generated MCP server launch must pin uv to the baked
image venv so `uv run` (cwd = workspace) reuses /app/.venv instead of
re-syncing the full dependency set (~350MB) on every spawn."""
@pytest.mark.asyncio
async def test_every_mcp_server_env_pins_uv_project_environment(self) -> None:
orch = AgentOrchestrator.__new__(AgentOrchestrator)
# be-dev-1 is a known agent (resolves role + uuid); generation is
# otherwise pure (writes a json file and returns its path).
config_path = await orch._generate_mcp_config("be-dev-1")
config = json.loads(Path(config_path).read_text())
servers = config["mcpServers"]
assert servers, "expected at least the four core MCP servers"
for name, spec in servers.items():
assert spec["command"] == "uv", f"{name} should launch via uv"
assert spec["env"].get("UV_PROJECT_ENVIRONMENT") == "/app/.venv", (
f"MCP server {name!r} is missing UV_PROJECT_ENVIRONMENT="
f"/app/.venv — without it `uv run` re-downloads deps into a "
f"cwd-relative venv on every spawn (#179). env={spec['env']}"
)
# Pinning the env location is necessary but NOT sufficient: from a
# workspace-clone cwd `uv run` still discovers the clone project and
# re-syncs the pinned venv against its drifted lock, which stalls and
# leaves the server stuck at status="pending" (zero gateway verbs).
# `--no-sync` skips that resync — it must come right after `run`.
assert spec["args"][:2] == ["run", "--no-sync"], (
f"MCP server {name!r} must launch with `uv run --no-sync ...` so "
f"a drifted workspace-clone lock can't trigger a resync stall; "
f"got args={spec['args']}"
)
@pytest.mark.asyncio
async def test_mcp_env_mirrors_flow_verb_timeout_settings(self) -> None:
"""flow_server.py (a subprocess, can't read Settings) mirrors the two
server-side flow-verb timeout budgets via env so its client timeout
stays coherent with operator tuning of either setting."""
orch = AgentOrchestrator.__new__(AgentOrchestrator)
config_path = await orch._generate_mcp_config("be-dev-1")
config = json.loads(Path(config_path).read_text())
env = config["mcpServers"]["roboco-flow"]["env"]
assert env["ROBOCO_FLOW_VERB_TIMEOUT_SECONDS"] == str(
settings.flow_verb_timeout_seconds
)
assert env["ROBOCO_FLOW_VERB_SLOW_TIMEOUT_SECONDS"] == str(
settings.flow_verb_slow_timeout_seconds
)