docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering authentication surfaces, outbound-call safety invariants, and other project conventions to guide Claude's understanding of critical system behaviors.
This commit is contained in:
@@ -6,12 +6,32 @@ Uses the FastMCP in-memory client (no network, no mounted app).
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from config import Settings
|
||||
import db.database as dbmod
|
||||
from config import Settings, get_settings
|
||||
from core.dial_plan import is_emergency_number
|
||||
from core.gateway import AIPSTNGateway
|
||||
from db.database import Base
|
||||
from mcp_server.server import create_mcp_server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mem_db(monkeypatch):
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
poolclass=StaticPool,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
monkeypatch.setattr(dbmod, "_engine", engine)
|
||||
monkeypatch.setattr(dbmod, "_session_factory", factory)
|
||||
yield factory
|
||||
await engine.dispose()
|
||||
|
||||
EXPECTED_TOOLS = {
|
||||
"make_call",
|
||||
"get_call_status",
|
||||
@@ -43,11 +63,64 @@ class TestToolSurface:
|
||||
tools = {t.name for t in await client.list_tools()}
|
||||
assert tools == EXPECTED_TOOLS
|
||||
|
||||
async def test_auth_configured_when_token_given(self):
|
||||
assert create_mcp_server(lambda: None, api_token="sekrit").auth is not None
|
||||
async def test_no_fastmcp_auth_configured(self):
|
||||
# Auth for /mcp is enforced by the ASGI _owner_only_mcp guard in
|
||||
# main.py, not on the FastMCP instance itself.
|
||||
assert create_mcp_server(lambda: None).auth is None
|
||||
|
||||
|
||||
class TestMcpOwnerGuard:
|
||||
"""The ASGI wrapper gates /mcp before the inner app runs."""
|
||||
|
||||
def _wrapped(self):
|
||||
import main
|
||||
|
||||
calls = {"inner": 0}
|
||||
|
||||
async def inner(scope, receive, send):
|
||||
calls["inner"] += 1
|
||||
await send({"type": "http.response.start", "status": 200, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"ok"})
|
||||
|
||||
return main._owner_only_mcp(inner), calls
|
||||
|
||||
async def _run(self, app, headers):
|
||||
sent = []
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
async def send(msg):
|
||||
sent.append(msg)
|
||||
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/mcp/",
|
||||
"headers": headers,
|
||||
"query_string": b"",
|
||||
}
|
||||
await app(scope, receive, send)
|
||||
status = next(m["status"] for m in sent if m["type"] == "http.response.start")
|
||||
return status
|
||||
|
||||
async def test_missing_token_401(self, monkeypatch, mem_db):
|
||||
monkeypatch.setattr(get_settings().casdoor, "enabled", True)
|
||||
monkeypatch.setattr(get_settings().casdoor, "endpoint", "https://id.example.test")
|
||||
monkeypatch.setattr(get_settings(), "owner_name", "owner@example.test")
|
||||
app, calls = self._wrapped()
|
||||
status = await self._run(app, headers=[])
|
||||
assert status == 401
|
||||
assert calls["inner"] == 0
|
||||
|
||||
async def test_dev_owner_passes(self, monkeypatch, mem_db):
|
||||
monkeypatch.setattr(get_settings().casdoor, "enabled", False)
|
||||
app, calls = self._wrapped()
|
||||
status = await self._run(app, headers=[])
|
||||
assert status == 200
|
||||
assert calls["inner"] == 1
|
||||
|
||||
|
||||
class TestGatewayResolution:
|
||||
async def test_tool_errors_cleanly_before_gateway_ready(self):
|
||||
mcp = create_mcp_server(lambda: None)
|
||||
|
||||
Reference in New Issue
Block a user