4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
274 lines
9.7 KiB
Python
274 lines
9.7 KiB
Python
"""Unit tests for the groundcover MCP service client.
|
|
|
|
The transport is exercised by patching ``GroundcoverClient._session`` with a
|
|
fake MCP session, so these tests cover normalization, redaction, JSON/SSE-style
|
|
payload parsing, cached query-reference behavior, and verification logic without
|
|
any network access.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from mcp import types
|
|
|
|
import integrations.groundcover.client as gc_client
|
|
from integrations.config_models import GroundcoverIntegrationConfig
|
|
|
|
|
|
def _config(**overrides: Any) -> GroundcoverIntegrationConfig:
|
|
base: dict[str, Any] = {"api_key": "tok-secret", "mcp_url": "https://mcp.example.com/api/mcp"}
|
|
base.update(overrides)
|
|
return GroundcoverIntegrationConfig.model_validate(base)
|
|
|
|
|
|
def _text_result(text: str, *, is_error: bool = False) -> types.CallToolResult:
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text=text)],
|
|
isError=is_error,
|
|
)
|
|
|
|
|
|
def _patch_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
client: gc_client.GroundcoverClient,
|
|
*,
|
|
tools: list[str] | None = None,
|
|
results: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""Patch ``client._session`` to yield a fake MCP session."""
|
|
tool_names = tools or []
|
|
tool_results = results or {}
|
|
|
|
class _FakeSession:
|
|
async def initialize(self) -> None:
|
|
return None
|
|
|
|
async def list_tools(self) -> Any:
|
|
return SimpleNamespace(tools=[SimpleNamespace(name=name) for name in tool_names])
|
|
|
|
async def call_tool(self, name: str, _arguments: dict[str, Any]) -> Any:
|
|
outcome = tool_results.get(name)
|
|
if isinstance(outcome, Exception):
|
|
raise outcome
|
|
return outcome
|
|
|
|
@asynccontextmanager
|
|
async def _fake_session() -> Any:
|
|
yield _FakeSession()
|
|
|
|
monkeypatch.setattr(client, "_session", _fake_session)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_reference_cache() -> None:
|
|
gc_client._REFERENCE_CACHE.clear()
|
|
|
|
|
|
def test_not_configured_returns_unavailable_without_network() -> None:
|
|
client = gc_client.GroundcoverClient(_config(api_key=""))
|
|
result = client.call_tool("query_logs", {"query": "* | limit 10"})
|
|
assert result.success is False
|
|
assert "not configured" in (result.error or "")
|
|
|
|
|
|
def test_call_tool_parses_json_array_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
results={"query_logs": _text_result('[{"level":"error","msg":"boom"}]')},
|
|
)
|
|
result = client.call_tool("query_logs", {"query": "* | limit 10"})
|
|
assert result.success is True
|
|
assert result.data == [{"level": "error", "msg": "boom"}]
|
|
assert result.notes == []
|
|
|
|
|
|
def test_call_tool_keeps_trailing_guidance_as_notes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
payload = '[{"a":1}]\nResults truncated at 1 rows. Use more specific filters.'
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(monkeypatch, client, results={"query_logs": _text_result(payload)})
|
|
result = client.call_tool("query_logs", {"query": "* | limit 1"})
|
|
assert result.data == [{"a": 1}]
|
|
assert result.notes and "truncated" in result.notes[0].lower()
|
|
|
|
|
|
def test_call_tool_error_is_redacted(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
leaky = "auth failed for Bearer tok-secret using tok-secret"
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
results={"query_logs": _text_result(leaky, is_error=True)},
|
|
)
|
|
result = client.call_tool("query_logs", {"query": "* | limit 1"})
|
|
assert result.success is False
|
|
assert "tok-secret" not in (result.error or "")
|
|
assert "Bearer ***" in (result.error or "")
|
|
|
|
|
|
def test_get_query_reference_caches(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
calls = {"n": 0}
|
|
client = gc_client.GroundcoverClient(_config())
|
|
|
|
class _FakeSession:
|
|
async def initialize(self) -> None:
|
|
return None
|
|
|
|
async def call_tool(self, _name: str, _arguments: dict[str, Any]) -> Any:
|
|
calls["n"] += 1
|
|
return _text_result("# gcQL reference")
|
|
|
|
@asynccontextmanager
|
|
async def _fake_session() -> Any:
|
|
yield _FakeSession()
|
|
|
|
monkeypatch.setattr(client, "_session", _fake_session)
|
|
|
|
first = client.get_query_reference()
|
|
second = client.get_query_reference()
|
|
assert first["reference"] == "# gcQL reference"
|
|
assert first["cached"] is False
|
|
assert second["cached"] is True
|
|
assert calls["n"] == 1
|
|
|
|
|
|
def test_list_workspaces_normalizes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod"]}]
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
result = client.list_workspaces()
|
|
assert result["success"] is True
|
|
assert result["total"] == 1
|
|
assert result["workspaces"][0]["org_name"] == "Acme"
|
|
|
|
|
|
def test_probe_missing_when_not_configured() -> None:
|
|
probe = gc_client.GroundcoverClient(_config(api_key="")).probe_access()
|
|
assert probe.status == "missing"
|
|
|
|
|
|
def test_probe_fails_when_required_tools_absent(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(monkeypatch, client, tools=["query_logs"])
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "list_workspaces" in probe.detail
|
|
|
|
|
|
def test_probe_fails_when_no_workspaces(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference"],
|
|
results={"list_workspaces": _text_result(_json([]))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "no workspaces" in probe.detail.lower()
|
|
|
|
|
|
def test_probe_reports_tenant_ambiguity(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [
|
|
{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod"]},
|
|
{"tenant_uuid": "t2", "org_name": "Beta", "backends": ["prod"]},
|
|
]
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "GROUNDCOVER_TENANT_UUID" in probe.detail
|
|
|
|
|
|
def test_probe_reports_backend_ambiguity(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod", "staging"]}]
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "GROUNDCOVER_BACKEND_ID" in probe.detail
|
|
|
|
|
|
def test_probe_rejects_unknown_configured_tenant(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod"]}]
|
|
client = gc_client.GroundcoverClient(_config(tenant_uuid="does-not-exist"))
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "GROUNDCOVER_TENANT_UUID" in probe.detail
|
|
assert "does-not-exist" in probe.detail
|
|
|
|
|
|
def test_probe_rejects_unknown_configured_backend(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod", "staging"]}]
|
|
client = gc_client.GroundcoverClient(_config(tenant_uuid="t1", backend_id="typo"))
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "failed"
|
|
assert "GROUNDCOVER_BACKEND_ID" in probe.detail
|
|
|
|
|
|
def test_probe_passes_with_valid_explicit_routing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod", "staging"]}]
|
|
client = gc_client.GroundcoverClient(_config(tenant_uuid="t1", backend_id="prod"))
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference", "query_logs"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "passed"
|
|
|
|
|
|
def test_probe_passes_single_workspace(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
workspaces = [{"tenant_uuid": "t1", "org_name": "Acme", "backends": ["prod"]}]
|
|
client = gc_client.GroundcoverClient(_config())
|
|
_patch_session(
|
|
monkeypatch,
|
|
client,
|
|
tools=["list_workspaces", "get_gcql_reference", "query_logs"],
|
|
results={"list_workspaces": _text_result(_json(workspaces))},
|
|
)
|
|
probe = client.probe_access()
|
|
assert probe.status == "passed"
|
|
# Detail names the connection target + workspace; assert on stable, non-URL
|
|
# tokens (a host-substring check trips CodeQL's URL-sanitization heuristic).
|
|
assert probe.detail.startswith("Connected to ")
|
|
assert "Acme" in probe.detail
|
|
|
|
|
|
def _json(value: Any) -> str:
|
|
import json
|
|
|
|
return json.dumps(value)
|