Files
MobilityOps/backend/tests/test_mcp_integrations.py
T
NuklearRabbitandClaude Sonnet 5 727c19a779 M9: MCP Hub locale/correlation propagation, real Hub health check, fix stale test image
Fixed two concrete gaps in the MCP knowledge-search endpoint: no locale field
existed at all (now nl-BE/en-GB/fr-BE, wired to the knowledge provider's
existing language param), and the correlation ID was always freshly minted,
ignoring any inbound X-Correlation-Id header. Added a shared dependency and
applied it to all four MCP endpoints so Fleet Ops's own audit log preserves
the Hub's real correlation ID end to end.

MCP_HUB_BASE_URL/MCP_PROVIDER_ID were declared in .env.example but never read
anywhere. Since the Hub's own registration is catalog-driven (it never needs
Fleet Ops to push a registration call), wired mcp_hub_base_url for a real Hub
reachability health check instead of an unneeded self-registration call.

Renamed Fleet Ops's own internal audit tool labels mobilityops_* -> fleet_ops_*
(mirrored in contracts/mcp-tools.json with mobilityops_* kept as deprecated
aliases); documented that the live Hub connector's own dotted tool namespace
is a separate, Hub-owned naming layer, deliberately not touched.

Automation page's MCP card now shows real evidence (last tool/client/count/
timestamp, honest no-evidence state) instead of just the registration flag.

Also fixed a real methodology gap found mid-session: compose.yaml's api
service has no bind mount, so `docker compose run --rm api` silently tests a
stale image until rebuilt. Re-ran every local gate after rebuilding; fixed one
genuinely stale test assertion and two lint line-length errors surfaced by
that rebuild. 176 tests passing, ruff clean, mypy clean (50 files).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 13:30:24 +02:00

134 lines
4.8 KiB
Python

from app.core.config import get_settings
def _headers(token: str | None = None, client_id: str = "test-mcp-client"):
settings = get_settings()
return {
"X-Service-Token": token if token is not None else settings.mcp_hub_service_token,
"X-Client-Id": client_id,
}
def test_operations_summary_requires_service_token(client):
response = client.get(
"/api/v1/integrations/mcp/operations-summary", headers=_headers(token="wrong")
)
assert response.status_code == 401
def test_operations_summary_returns_metrics(client):
response = client.get("/api/v1/integrations/mcp/operations-summary", headers=_headers())
assert response.status_code == 200
body = response.json()
assert "metrics" in body
assert body["metrics"]["available"] >= 0
def test_attention_vehicles_filters_by_severity(client):
response = client.get(
"/api/v1/integrations/mcp/attention-vehicles",
params={"minimum_severity": "high", "limit": 50},
headers=_headers(),
)
assert response.status_code == 200
rows = response.json()
assert all(r["severity"] == "high" for r in rows)
def test_attention_vehicles_respects_limit(client):
response = client.get(
"/api/v1/integrations/mcp/attention-vehicles",
params={"minimum_severity": "low", "limit": 2},
headers=_headers(),
)
assert response.status_code == 200
assert len(response.json()) <= 2
def test_vehicle_details_known_ref(client):
response = client.get(
"/api/v1/integrations/mcp/vehicles/MO-016", headers=_headers()
)
assert response.status_code == 200
body = response.json()
assert body["public_ref"] == "MO-016"
assert "registration_number" not in body # narrow read-only view, not the full record
def test_vehicle_details_unknown_ref_is_404(client):
response = client.get(
"/api/v1/integrations/mcp/vehicles/MO-999", headers=_headers()
)
assert response.status_code == 404
def test_search_knowledge_grounded_and_respects_max_sources(client):
response = client.post(
"/api/v1/integrations/mcp/search-knowledge",
json={"question": "What must I do when a vehicle returns with damage?", "max_sources": 1},
headers=_headers(),
)
assert response.status_code == 200
body = response.json()
assert body["evidence_state"] == "grounded"
assert len(body["sources"]) == 1
def test_mcp_tool_requests_are_audited(client, ops_client):
client.get("/api/v1/integrations/mcp/operations-summary", headers=_headers(client_id="probe-1"))
events = ops_client.get("/api/v1/audit", params={"action": "mcp_tool_request"}).json()
assert len(events) >= 1
assert events[0]["actor_type"] == "service"
def test_search_knowledge_respects_requested_locale(client):
response = client.post(
"/api/v1/integrations/mcp/search-knowledge",
json={
"question": "Wat moet ik doen wanneer een voertuig beschadigd terugkomt?",
"max_sources": 1,
"locale": "nl-BE",
},
headers=_headers(),
)
assert response.status_code == 200
body = response.json()
assert body["evidence_state"] == "grounded"
def test_search_knowledge_preserves_inbound_correlation_id(client, ops_client):
inbound = "11111111-1111-1111-1111-111111111111"
response = client.post(
"/api/v1/integrations/mcp/search-knowledge",
json={"question": "What must I do when a vehicle returns with damage?", "max_sources": 1},
headers={**_headers(client_id="correlation-probe"), "X-Correlation-Id": inbound},
)
assert response.status_code == 200
assert response.json()["correlation_id"] == inbound
events = ops_client.get("/api/v1/audit", params={"action": "mcp_tool_request"}).json()
matching = [e for e in events if e["correlation_id"] == inbound]
assert len(matching) == 1
def test_operations_summary_mints_correlation_id_when_none_supplied(client, ops_client):
response = client.get(
"/api/v1/integrations/mcp/operations-summary",
headers=_headers(client_id="no-correlation-probe"),
)
assert response.status_code == 200
events = ops_client.get("/api/v1/audit", params={"action": "mcp_tool_request"}).json()
matching = [e for e in events if e["actor_label"] == "no-correlation-probe"]
assert len(matching) >= 1
assert matching[0]["correlation_id"] # a fresh UUID was minted, not left empty
def test_no_write_endpoints_exist_under_mcp_namespace(client):
for method, path in [
("post", "/api/v1/integrations/mcp/vehicles/MO-016"),
("put", "/api/v1/integrations/mcp/vehicles/MO-016"),
("delete", "/api/v1/integrations/mcp/vehicles/MO-016"),
]:
response = getattr(client, method)(path, headers=_headers())
assert response.status_code in (404, 405)