rmi-backend/tests/unit/test_tier1_moat_mcp.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

135 lines
5.7 KiB
Python

"""M3 moat TIER 1 - tests for new MCP tools (analytics_query, mcp_discover, status_check).
These tests verify the tool catalog, versioning, and dispatch - all without
needing a live database or external service. They test the contract.
"""
from __future__ import annotations
from app.mcp.server import (
MCP_PROTOCOL_VERSION,
MCP_SERVER_VERSION,
TOOL_CATALOG,
TOOL_DEPRECATED,
TOOL_SUCCESSORS,
TOOL_VERSIONS,
)
# ── Catalog completeness ─────────────────────────────────────────
def test_tool_catalog_has_minimum_count():
"""MCP catalog must have at least 8 original tools + 3 new TIER 1 tools = 11+."""
assert len(TOOL_CATALOG) >= 11, f"only {len(TOOL_CATALOG)} tools, expected 11+"
def test_new_tier1_tools_present():
"""The 3 new TIER 1 moat tools must be in the catalog."""
names = {t["name"] for t in TOOL_CATALOG}
assert "analytics_query" in names, "analytics_query tool missing"
assert "mcp_discover" in names, "mcp_discover tool missing"
assert "status_check" in names, "status_check tool missing"
def test_all_tools_have_input_schema():
"""Every tool must declare an inputSchema (JSON Schema 2020-12)."""
for tool in TOOL_CATALOG:
assert "inputSchema" in tool, f"{tool.get('name')} missing inputSchema"
assert tool["inputSchema"].get("type") == "object", (
f"{tool['name']} inputSchema.type must be 'object'"
)
def test_all_tools_have_descriptions():
"""Every tool must have a human-readable description."""
for tool in TOOL_CATALOG:
assert tool.get("description"), f"{tool.get('name')} missing description"
assert len(tool["description"]) > 20, f"{tool['name']} description too short"
def test_all_tools_have_versions():
"""Every tool must have a semantic version in TOOL_VERSIONS."""
catalog_names = {t["name"] for t in TOOL_CATALOG}
versioned_names = set(TOOL_VERSIONS.keys())
missing = catalog_names - versioned_names
assert not missing, f"tools missing versions: {missing}"
def test_version_format_is_semver():
"""All tool versions must be MAJOR.MINOR.PATCH."""
import re
semver_re = re.compile(r"^\d+\.\d+\.\d+$")
for name, ver in TOOL_VERSIONS.items():
assert semver_re.match(ver), f"{name} version '{ver}' is not semver"
def test_bayesian_reputation_version_bumped():
"""M3 - deployer reputation must be v2.x (Bayesian upgrade)."""
ver = TOOL_VERSIONS.get("get_deployer_reputation", "")
assert ver.startswith("2."), f"get_deployer_reputation should be v2.x (Bayesian), got {ver}"
def test_generate_report_version_bumped():
"""M3 - generate_report must be v2.x (RAG-grounded)."""
ver = TOOL_VERSIONS.get("generate_report", "")
assert ver.startswith("2."), f"generate_report should be v2.x (RAG-grounded), got {ver}"
# ── Deprecation registry ─────────────────────────────────────────
def test_deprecated_set_is_valid():
"""Every deprecated tool must also be in TOOL_CATALOG."""
for name in TOOL_DEPRECATED:
catalog_names = {t["name"] for t in TOOL_CATALOG}
assert name in catalog_names, f"deprecated tool {name} not in catalog"
def test_successor_targets_exist():
"""Every successor must point to a tool that exists in the catalog."""
catalog_names = {t["name"] for t in TOOL_CATALOG}
for old, new in TOOL_SUCCESSORS.items():
assert new in catalog_names, f"successor '{new}' for '{old}' not in catalog"
# ── Server metadata ──────────────────────────────────────────────
def test_server_version_is_set():
"""Server version must be a non-empty semantic version."""
import re
assert re.match(r"^\d+\.\d+\.\d+$", MCP_SERVER_VERSION), (
f"MCP_SERVER_VERSION '{MCP_SERVER_VERSION}' is not semver"
)
def test_protocol_version_is_set():
"""MCP protocol version must be set (2024-11-05 is the current spec)."""
assert MCP_PROTOCOL_VERSION
assert len(MCP_PROTOCOL_VERSION) > 5
# ── DuckDB max_rows safety ────────────────────────────────────────
def test_duckdb_query_supports_max_rows():
"""DuckDB query() must support max_rows to prevent MCP API DoS."""
import inspect
from app.core.duckdb_analytics import DuckDBAnalytics
sig = inspect.signature(DuckDBAnalytics.query)
assert "max_rows" in sig.parameters, "DuckDBAnalytics.query must support max_rows parameter"
def test_duckdb_max_rows_caps_results():
"""When max_rows is set, fetchmany is used (not fetchall)."""
from app.core.duckdb_analytics import DuckDBAnalytics
d = DuckDBAnalytics()
# Insert 5 rows
d._conn.execute("CREATE TABLE IF NOT EXISTS _test_max_rows AS SELECT 1 AS n")
d._conn.execute("INSERT INTO _test_max_rows VALUES (1),(2),(3),(4),(5)")
rows = d.query("SELECT * FROM _test_max_rows ORDER BY n", max_rows=2)
assert len(rows) == 2, f"max_rows=2 should return 2 rows, got {len(rows)}"
d._conn.execute("DROP TABLE _test_max_rows")
# ── Tool name format ─────────────────────────────────────────────
def test_tool_names_are_kebab_case():
"""Tool names should be lowercase with underscores (per MCP convention)."""
import re
name_re = re.compile(r"^[a-z][a-z0-9_]*$")
for tool in TOOL_CATALOG:
name = tool["name"]
assert name_re.match(name), f"tool name '{name}' is not kebab/snake_case"