Files
Cortex-Inara/cortex/tests/test_api_chat.py
Scott Idem 92a8f5d894 test: add Cortex test suite (77 tests, no LLM calls)
Tests cover:
  - Smoke: /health, /auth/status, /distill/status (test_health.py)
  - Persona validation: path traversal, bad names, list_personas (test_persona.py)
  - Chat API: persona routing, session persistence, error handling (test_api_chat.py)
  - Files API: ALLOWED set enforcement, read/write, missing files (test_api_files.py)
  - Webhooks: NC Talk HMAC accept/reject, Google Chat JWT (test_webhooks.py)
  - Tools: scratch read/write/append/clear, tasks CRUD, cron parser + tools (test_tools.py)
  - Security: path traversal, replay attack, known gaps documented (test_security.py)

All LLM calls mocked — suite runs in ~1.4s.
Run: cd cortex && .venv/bin/pytest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 22:03:42 -04:00

123 lines
4.0 KiB
Python

"""
Tests for POST /chat — persona routing, session handling, LLM mocking.
"""
import json
import pytest
def _parse_sse(text: str) -> list[dict]:
"""Extract JSON payloads from an SSE response body."""
events = []
for line in text.splitlines():
if line.startswith("data: "):
try:
events.append(json.loads(line[6:]))
except json.JSONDecodeError:
pass
return events
@pytest.mark.anyio
async def test_chat_basic(client, mock_llm):
r = await client.post("/chat", json={"message": "Hello", "persona": "inara"})
assert r.status_code == 200
events = _parse_sse(r.text)
responses = [e for e in events if e.get("type") == "response"]
assert len(responses) == 1
assert responses[0]["response"] == "Hello, I am a test response."
assert responses[0]["backend"] == "claude"
assert "session_id" in responses[0]
@pytest.mark.anyio
async def test_chat_default_persona(client, mock_llm):
"""persona defaults to 'inara' when not specified."""
r = await client.post("/chat", json={"message": "Hi"})
assert r.status_code == 200
events = _parse_sse(r.text)
assert any(e.get("type") == "response" for e in events)
@pytest.mark.anyio
async def test_chat_unknown_persona(client, mock_llm):
r = await client.post("/chat", json={"message": "Hi", "persona": "nobody"})
assert r.status_code == 200
events = _parse_sse(r.text)
errors = [e for e in events if e.get("type") == "error"]
assert len(errors) == 1
assert "nobody" in errors[0]["message"]
@pytest.mark.anyio
async def test_chat_path_traversal_persona(client, mock_llm):
r = await client.post("/chat", json={"message": "Hi", "persona": "../../etc"})
assert r.status_code == 200
events = _parse_sse(r.text)
errors = [e for e in events if e.get("type") == "error"]
assert len(errors) == 1
@pytest.mark.anyio
async def test_chat_session_persists(client, mock_llm):
"""Same session_id reuses history."""
r1 = await client.post("/chat", json={"message": "First message", "session_id": "test-sess-1"})
r2 = await client.post("/chat", json={"message": "Second message", "session_id": "test-sess-1"})
assert r1.status_code == 200
assert r2.status_code == 200
@pytest.mark.anyio
async def test_sessions_list(client, mock_llm):
"""After a chat, the session appears in /sessions."""
await client.post("/chat", json={"message": "Hello", "session_id": "test-list-sess"})
r = await client.get("/sessions")
assert r.status_code == 200
sessions = r.json()["sessions"]
ids = [s["session_id"] for s in sessions]
assert "test-list-sess" in ids
@pytest.mark.anyio
async def test_session_history(client, mock_llm):
await client.post("/chat", json={"message": "Hello", "session_id": "test-hist-sess"})
r = await client.get("/history/test-hist-sess")
assert r.status_code == 200
msgs = r.json()["messages"]
assert any(m["role"] == "user" and "Hello" in m["content"] for m in msgs)
@pytest.mark.anyio
async def test_session_delete(client, mock_llm):
await client.post("/chat", json={"message": "Hello", "session_id": "test-del-sess"})
r = await client.delete("/sessions/test-del-sess")
assert r.status_code == 200
assert r.json()["ok"] is True
@pytest.mark.anyio
async def test_session_delete_unknown(client):
r = await client.delete("/sessions/does-not-exist")
assert r.status_code == 404
@pytest.mark.anyio
async def test_backend_get(client):
r = await client.get("/backend")
assert r.status_code == 200
assert r.json()["primary"] in ("claude", "gemini")
@pytest.mark.anyio
async def test_backend_set(client):
r = await client.post("/backend", json={"primary": "gemini"})
assert r.status_code == 200
assert r.json()["primary"] == "gemini"
# Reset
await client.post("/backend", json={"primary": "claude"})
@pytest.mark.anyio
async def test_backend_set_invalid(client):
r = await client.post("/backend", json={"primary": "gpt-4"})
assert r.status_code == 400