- Added configuration options for requiring human approval before applying LLM-generated MCP patches. - Updated Docker setup to include skills directory. - Integrated skills management into the backend, allowing for procedural guides and skill matching. - Refactored database initialization to apply Alembic migrations. - Enhanced task approval process to handle MCP patch applications with optional approval. - Introduced new schemas for skills and updated existing APIs to support skills functionality. This commit lays the groundwork for improved agent capabilities and better management of MCP development processes.
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""Tests for MCP patch approval gating."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
|
|
|
|
|
def test_repair_queues_apply_patch_when_approval_required(monkeypatch):
|
|
from app.config import get_settings, settings
|
|
|
|
get_settings.cache_clear()
|
|
|
|
proposal = {
|
|
"ok": True,
|
|
"summary": "Add missing tool",
|
|
"commit_message": "fix: add tool",
|
|
"files": [{"path": "server.py", "content": "# patched"}],
|
|
}
|
|
|
|
mock_db = MagicMock()
|
|
mock_db.commit = AsyncMock()
|
|
mock_session = MagicMock()
|
|
mock_session.__aenter__ = AsyncMock(return_value=mock_db)
|
|
mock_session.__aexit__ = AsyncMock(return_value=None)
|
|
|
|
async def _run():
|
|
with (
|
|
patch.object(settings, "mcp_dev_require_approval", True),
|
|
patch("app.agent.mcp_development.propose_mcp_patch", new=AsyncMock(return_value=proposal)),
|
|
patch(
|
|
"app.agent.mcp_development.backup_proposal_files",
|
|
new=AsyncMock(return_value={"server.py": "# old"}),
|
|
),
|
|
patch("app.agent.mcp_development.execute_approved_patch", new=AsyncMock()) as mock_apply,
|
|
patch("app.database.SessionLocal", return_value=mock_session),
|
|
):
|
|
from app.agent.mcp_development import attempt_mcp_repair
|
|
|
|
emit = AsyncMock()
|
|
result = await attempt_mcp_repair(
|
|
task_id=42,
|
|
server="pfsense-mcp",
|
|
tool="pfsense_list_rules",
|
|
args={"interface": "wan"},
|
|
issue="tool missing",
|
|
error="unknown tool",
|
|
output=None,
|
|
manager=AsyncMock(),
|
|
emit=emit,
|
|
device="pfsense",
|
|
)
|
|
return result, mock_apply, emit, mock_db
|
|
|
|
result, mock_apply, emit, mock_db = asyncio.run(_run())
|
|
|
|
assert result.get("pending_approval") is True
|
|
assert result.get("approval_type") == "mcp_apply_patch"
|
|
mock_apply.assert_not_called()
|
|
mock_db.add.assert_called_once()
|
|
mock_db.commit.assert_awaited_once()
|
|
assert any(c.args[1] == "approval" for c in emit.call_args_list)
|
|
|
|
|
|
def test_repair_applies_immediately_when_approval_not_required(monkeypatch):
|
|
from app.config import get_settings, settings
|
|
|
|
get_settings.cache_clear()
|
|
|
|
proposal = {
|
|
"ok": True,
|
|
"summary": "Fix crash",
|
|
"files": [{"path": "server.py", "content": "# patched"}],
|
|
}
|
|
|
|
async def _run():
|
|
with (
|
|
patch.object(settings, "mcp_dev_require_approval", False),
|
|
patch("app.agent.mcp_development.propose_mcp_patch", new=AsyncMock(return_value=proposal)),
|
|
patch(
|
|
"app.agent.mcp_development.execute_approved_patch",
|
|
new=AsyncMock(return_value={"ok": True, "tools": ["a"]}),
|
|
) as mock_apply,
|
|
):
|
|
from app.agent.mcp_development import attempt_mcp_repair
|
|
|
|
return await attempt_mcp_repair(
|
|
task_id=1,
|
|
server="pfsense-mcp",
|
|
tool="t",
|
|
args={},
|
|
issue="x",
|
|
error="err",
|
|
output=None,
|
|
manager=AsyncMock(),
|
|
emit=AsyncMock(),
|
|
), mock_apply
|
|
|
|
result, mock_apply = asyncio.run(_run())
|
|
|
|
assert result.get("ok") is True
|
|
mock_apply.assert_awaited_once()
|