test: add unit tests for all 10 routers (92 total)
New test files: - test_conversations.py — list/create/get/update/delete/delete-all, admin enforcement - test_presets.py — list/create/update/delete, default preset protection - test_profile.py — get/update/default, length validation - test_models_router.py — list/ps/show/stats/search-status, connect errors - test_completions.py — API key auth, FIM passthrough, streaming/blocking, errors - test_search_route.py — explicit search flow, no results, stream errors - test_memories.py — edit/search/stats endpoints, validation, admin enforcement Update AGENTS.md with full test file coverage table and README.md
This commit is contained in:
222
tests/test_completions.py
Normal file
222
tests/test_completions.py
Normal file
@@ -0,0 +1,222 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app
|
||||
import config
|
||||
import db
|
||||
import routers.completions
|
||||
from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS
|
||||
|
||||
|
||||
def make_client(tmp_path: Path) -> TestClient:
|
||||
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
|
||||
db.DB_PATH = tmp_path / "jarvischat-completions.db"
|
||||
SESSIONS.clear()
|
||||
PIN_ATTEMPTS.clear()
|
||||
RATE_EVENTS.clear()
|
||||
db.init_db()
|
||||
return TestClient(app.app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
TEST_API_KEY = "test-sk-jarvischat-completions"
|
||||
|
||||
|
||||
def _auth_headers(extra: dict = None) -> dict:
|
||||
h = {"Authorization": f"Bearer {TEST_API_KEY}", "Content-Type": "application/json", "Origin": "http://testserver"}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
class _MockStreamResponse:
|
||||
def __init__(self, lines: list[str]):
|
||||
self._lines = lines
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def aiter_lines(self):
|
||||
for line in self._lines:
|
||||
yield line
|
||||
|
||||
|
||||
class _MockAsyncPostResponse:
|
||||
def __init__(self, status_code=200, json_data=None):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data or {}
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
|
||||
def _stream_json_lines(events: list[dict]) -> list[str]:
|
||||
return [json.dumps(event) for event in events]
|
||||
|
||||
|
||||
def test_completions_missing_api_key(tmp_path: Path):
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}]},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_completions_invalid_api_key(tmp_path: Path):
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}]},
|
||||
headers={"Authorization": "Bearer wrong-key", "Origin": "http://testserver"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_completions_no_messages(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post("/v1/chat/completions", json={}, headers=_auth_headers())
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_completions_empty_messages(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post("/v1/chat/completions", json={"messages": []}, headers=_auth_headers())
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_completions_no_user_message(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "assistant", "content": "hello"}]},
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_completions_streaming(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
events = _stream_json_lines([
|
||||
{"choices": [{"delta": {"content": "Hello"}, "logprobs": None}]},
|
||||
{"choices": [{"delta": {"content": " world"}, "logprobs": None}]},
|
||||
{"choices": [{"delta": {}, "finish_reason": "stop"}], "usage": {"tokens_per_second": 15.0}},
|
||||
])
|
||||
|
||||
call_count = 0
|
||||
|
||||
def stream_stub(self, method, url, json=None, timeout=None):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return _MockStreamResponse(events)
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "stream", stream_stub)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
assert "data: [DONE]" in body
|
||||
assert "Hello" in body or "world" in body
|
||||
assert "chatcmpl-" in body
|
||||
|
||||
|
||||
def test_completions_blocking(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
events = _stream_json_lines([
|
||||
{"choices": [{"delta": {"content": "Hello world"}, "logprobs": None}]},
|
||||
{"choices": [{"delta": {}, "finish_reason": "stop"}], "usage": {}},
|
||||
])
|
||||
|
||||
def stream_stub(self, method, url, json=None, timeout=None):
|
||||
return _MockStreamResponse(events)
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "stream", stream_stub)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["object"] == "chat.completion"
|
||||
assert data["choices"][0]["message"]["content"] == "Hello world"
|
||||
|
||||
|
||||
def test_completions_fim_passthrough(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
fim_data = {"prompt": "def foo():\n ", "suffix": "\n return x", "model": "llama3.1:latest"}
|
||||
|
||||
async def mock_post(self, url, json=None, timeout=None):
|
||||
return _MockAsyncPostResponse(json_data={"choices": [{"text": "pass"}], "usage": {}})
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post("/v1/chat/completions", json=fim_data, headers=_auth_headers())
|
||||
assert resp.status_code == 200
|
||||
assert "choices" in resp.json()
|
||||
|
||||
|
||||
def test_completions_connect_error_stream(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
|
||||
def broken_stream(self, method, url, json=None, timeout=None):
|
||||
raise httpx.ConnectError("Connection refused")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "stream", broken_stream)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "connection_error" in resp.text
|
||||
|
||||
|
||||
def test_completions_connect_error_blocking(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
|
||||
def broken_stream(self, method, url, json=None, timeout=None):
|
||||
raise httpx.ConnectError("Connection refused")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "stream", broken_stream)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post(
|
||||
"/v1/chat/completions",
|
||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
def test_completions_fim_connect_error(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
|
||||
fim_data = {"prompt": "def foo():", "model": "llama3.1:latest"}
|
||||
|
||||
def broken_post(self, url, json=None, timeout=None):
|
||||
raise httpx.ConnectError("Connection refused")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", broken_post)
|
||||
|
||||
with make_client(tmp_path) as client:
|
||||
resp = client.post("/v1/chat/completions", json=fim_data, headers=_auth_headers())
|
||||
assert resp.status_code == 503
|
||||
Reference in New Issue
Block a user