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
223 lines
7.6 KiB
Python
223 lines
7.6 KiB
Python
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
|