Some checks failed
Code Review / ai-code-review (push) Successful in 10s
run-migration / migrate (push) Failing after 8s
CD Pipeline / tests (push) Successful in 1m14s
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from src.plugins.mcp import gateway as gateway_module
|
|
from src.plugins.mcp.gateway import GateCheckResult, GatewayContext, McpGateway
|
|
from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult
|
|
|
|
|
|
class FakeDb:
|
|
def __init__(self) -> None:
|
|
self.added: list[object] = []
|
|
self.flush_count = 0
|
|
|
|
def add(self, item: object) -> None:
|
|
self.added.append(item)
|
|
|
|
async def flush(self) -> None:
|
|
self.flush_count += 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_audit_persists_blocked_gate_without_tool_row() -> None:
|
|
db = FakeDb()
|
|
run_id = uuid.uuid4()
|
|
|
|
await McpGateway(db)._write_audit(
|
|
ctx=GatewayContext(
|
|
project_id="awoooi",
|
|
agent_id="openclaw-sre",
|
|
tool_name="missing_tool",
|
|
run_id=run_id,
|
|
trace_id="trace-audit-gap",
|
|
),
|
|
tool_row=None,
|
|
parameters={"namespace": "awoooi-prod"},
|
|
result=None,
|
|
gate_result=GateCheckResult(),
|
|
result_status="blocked",
|
|
block_gate=1,
|
|
block_reason="E-MCP-GATE-001: project blocked",
|
|
latency_ms=12,
|
|
)
|
|
|
|
assert db.flush_count == 1
|
|
assert len(db.added) == 1
|
|
audit = db.added[0]
|
|
assert audit.project_id == "awoooi"
|
|
assert audit.run_id == run_id
|
|
assert audit.tool_id is None
|
|
assert audit.tool_name == "missing_tool"
|
|
assert audit.result_status == "blocked"
|
|
assert audit.block_gate == 1
|
|
assert audit.gate_result["schema_version"] == "awooop_mcp_gateway_audit_v1"
|
|
assert audit.gate_result["gateway_path"] == "awooop_mcp_gateway"
|
|
assert audit.gate_result["policy_enforced"] is True
|
|
|
|
|
|
class FakeProvider(MCPToolProvider):
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, dict]] = []
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "kubernetes"
|
|
|
|
async def list_tools(self) -> list[MCPTool]:
|
|
return [MCPTool(name="kubectl_get", description="", input_schema={})]
|
|
|
|
async def execute(self, tool_name: str, parameters: dict) -> MCPToolResult:
|
|
self.calls.append((tool_name, parameters))
|
|
return MCPToolResult(success=True, execution_id="provider-ok", output={"ok": True})
|
|
|
|
|
|
class FakeProviderRegistry:
|
|
def __init__(self, provider: FakeProvider) -> None:
|
|
self.provider = provider
|
|
|
|
def get(self, _name: str):
|
|
return None
|
|
|
|
def all(self) -> list[FakeProvider]:
|
|
return [self.provider]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_tool_resolves_provider_by_tool_manifest(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
provider = FakeProvider()
|
|
monkeypatch.setattr(
|
|
gateway_module,
|
|
"get_provider_registry",
|
|
lambda: FakeProviderRegistry(provider),
|
|
)
|
|
|
|
result = await McpGateway(FakeDb())._execute_tool(
|
|
GatewayContext(
|
|
project_id="awoooi",
|
|
agent_id="pre_decision_investigator",
|
|
tool_name="kubectl_get",
|
|
trace_id="INC-GW",
|
|
),
|
|
SimpleNamespace(tool_name="kubectl_get"),
|
|
{
|
|
"namespace": "awoooi-prod",
|
|
"_mcp_audit": {
|
|
"incident_id": "INC-GW",
|
|
"session_id": "incident:INC-GW:pre_decision",
|
|
"flywheel_node": "sense",
|
|
"agent_role": "pre_decision_investigator",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result.success is True
|
|
assert provider.calls
|
|
tool_name, parameters = provider.calls[0]
|
|
assert tool_name == "kubectl_get"
|
|
assert parameters["_mcp_audit"]["gateway_path"] == "awooop_mcp_gateway"
|
|
assert parameters["_mcp_audit"]["incident_id"] == "INC-GW"
|
|
assert parameters["_mcp_audit"]["flywheel_node"] == "sense"
|