docs: add Claude AI assistant rules and configuration
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s

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:
2026-07-28 19:01:38 -04:00
parent 016d8be71d
commit 4a3c14d4af
40 changed files with 2851 additions and 202 deletions

View File

@@ -1,27 +1,57 @@
"""
WebSocket event-stream tests.
The socket is refused (4401) without the bearer token, and an
authorized client immediately receives the synthetic trunk-status
event followed by the replayed recent history.
The socket is owner-gated: refused (4401) when SSO is enabled and no
credential is supplied, and — in dev-owner mode (SSO disabled) — an
authorized client immediately receives the synthetic trunk-status event
followed by the replayed recent history.
The WS `_authorize` resolves the owner via a DB session, so an in-memory
SQLite database is wired in (StaticPool, shared across the TestClient
thread) mirroring tests/test_data_layer.py.
"""
import asyncio
import pytest
from pydantic import SecretStr
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from starlette.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
import db.database as dbmod
import main
from config import Settings, get_settings
from core.gateway import AIPSTNGateway
from db.database import Base
from models.events import EventType, GatewayEvent
@pytest.fixture
def ws_app(monkeypatch):
monkeypatch.setattr(get_settings(), "api_token", SecretStr("tok"))
def mem_db(monkeypatch):
"""Synchronous setup of an in-memory SQLite DB shared with the app."""
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
poolclass=StaticPool,
connect_args={"check_same_thread": False},
)
async def _create():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
asyncio.run(_create())
factory = async_sessionmaker(engine, expire_on_commit=False)
monkeypatch.setattr(dbmod, "_engine", engine)
monkeypatch.setattr(dbmod, "_session_factory", factory)
yield
asyncio.run(engine.dispose())
@pytest.fixture
def ws_app(monkeypatch, mem_db):
"""Dev-owner mode: a tokenless WS connect resolves the owner."""
monkeypatch.setattr(get_settings().casdoor, "enabled", False)
gateway = AIPSTNGateway(settings=Settings())
main.app.state.gateway = gateway
yield gateway
@@ -38,7 +68,11 @@ def _publish(gateway, call_id: str) -> None:
class TestEventStream:
def test_refused_without_token(self, ws_app):
def test_refused_without_credential(self, monkeypatch, mem_db):
"""SSO enabled + no token → the socket is closed with 4401."""
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")
client = TestClient(main.app)
with pytest.raises(WebSocketDisconnect) as exc:
with client.websocket_connect("/ws/events"):
@@ -50,7 +84,7 @@ class TestEventStream:
_publish(ws_app, "call_ws2")
client = TestClient(main.app)
with client.websocket_connect("/ws/events?token=tok") as ws:
with client.websocket_connect("/ws/events") as ws:
first = ws.receive_json()
assert first["type"] == EventType.SIP_TRUNK_REGISTRATION_FAILED.value
replayed = [ws.receive_json() for _ in range(2)]
@@ -58,9 +92,7 @@ class TestEventStream:
def test_per_call_stream_filters(self, ws_app):
client = TestClient(main.app)
with client.websocket_connect(
"/ws/calls/call_target/events?token=tok"
) as ws:
with client.websocket_connect("/ws/calls/call_target/events") as ws:
_publish(ws_app, "call_other")
_publish(ws_app, "call_target")
msg = ws.receive_json()