FastAPI app, upstream Confluence/model adapters, authoritative history, rootless container lifecycle, artifact storage and downloads, fake peers under backend/dev, tests under tests/backend. Root pytest.ini deselects the live marker by default; requirements gain the backend dependencies.
133 lines
4.4 KiB
Python
133 lines
4.4 KiB
Python
"""Unit tests for model adapters and dispatcher."""
|
|
|
|
import json
|
|
import pytest
|
|
import httpx
|
|
from backend.model import FakeModelAdapter, OpenAIModelAdapter, ModelDispatcher
|
|
from backend.errors import InvalidInputError, ModelContextExceededError
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fake_model_adapter():
|
|
adapter = FakeModelAdapter()
|
|
adapter.queue_response({
|
|
"content": [{"type": "text", "text": "Hello world"}],
|
|
"stop_reason": "stop",
|
|
"usage": {"input_tokens": 5, "output_tokens": 5},
|
|
})
|
|
|
|
dispatcher = ModelDispatcher(adapter, max_calls=2)
|
|
resp, err = await dispatcher.dispatch(
|
|
{"messages": [{"role": "user", "content": [{"type": "text", "text": "Hi"}]}], "tools": []},
|
|
system_instruction="System prompt",
|
|
)
|
|
assert err is None
|
|
assert resp["stop_reason"] == "stop"
|
|
assert resp["content"][0]["text"] == "Hello world"
|
|
assert adapter.call_count == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_model_dispatcher_budget():
|
|
adapter = FakeModelAdapter()
|
|
dispatcher = ModelDispatcher(adapter, max_calls=1)
|
|
|
|
# 1st call succeeds
|
|
resp1, err1 = await dispatcher.dispatch(
|
|
{"messages": [], "tools": []},
|
|
system_instruction="",
|
|
)
|
|
assert err1 is None
|
|
|
|
# 2nd call fails budget
|
|
resp2, err2 = await dispatcher.dispatch(
|
|
{"messages": [], "tools": []},
|
|
system_instruction="",
|
|
)
|
|
assert resp2 is None
|
|
assert err2 is not None
|
|
assert "limit" in err2["message"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_model_dispatcher_forged_handle():
|
|
adapter = FakeModelAdapter()
|
|
dispatcher = ModelDispatcher(adapter)
|
|
|
|
# Supply an assistant message with forged provider_state
|
|
resp, err = await dispatcher.dispatch(
|
|
{
|
|
"messages": [
|
|
{"role": "assistant", "content": [], "provider_state": "forged-uuid-1234"}
|
|
],
|
|
"tools": [],
|
|
},
|
|
system_instruction="",
|
|
)
|
|
assert resp is None
|
|
assert err is not None
|
|
assert err["code"] == "invalid_input"
|
|
assert "provider_state" in err["message"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_openai_adapter_translation():
|
|
async def handler(request: httpx.Request) -> httpx.Response:
|
|
assert request.headers.get("Authorization") == "Bearer test-key"
|
|
body = json.loads(request.read().decode("utf-8"))
|
|
assert body["model"] == "gpt-4o"
|
|
assert body["messages"][0]["role"] == "system"
|
|
assert body["messages"][0]["content"] == "Trusted system instruction"
|
|
assert body["messages"][1]["role"] == "user"
|
|
assert body["messages"][1]["content"] == "Hello"
|
|
|
|
# Return tool call response
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": None,
|
|
"tool_calls": [
|
|
{
|
|
"id": "tc_1",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "confluence_search",
|
|
"arguments": "{\"query\": \"deploy\"}",
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"finish_reason": "tool_calls",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 15, "completion_tokens": 20},
|
|
},
|
|
)
|
|
|
|
transport = httpx.MockTransport(handler)
|
|
adapter = OpenAIModelAdapter(
|
|
api_key="test-key",
|
|
model_name="gpt-4o",
|
|
transport=transport,
|
|
)
|
|
|
|
resp = await adapter.complete(
|
|
messages=[{"role": "user", "content": [{"type": "text", "text": "Hello"}]}],
|
|
tools=[{"name": "confluence_search", "description": "search", "input_schema": {}}],
|
|
system_instruction="Trusted system instruction",
|
|
)
|
|
|
|
assert resp["stop_reason"] == "tool_calls"
|
|
assert len(resp["content"]) == 1
|
|
assert resp["content"][0]["type"] == "tool_call"
|
|
assert resp["content"][0]["id"] == "tc_1"
|
|
assert resp["content"][0]["name"] == "confluence_search"
|
|
assert resp["content"][0]["arguments"] == {"query": "deploy"}
|
|
assert resp["usage"] == {"input_tokens": 15, "output_tokens": 20}
|
|
|
|
await adapter.close()
|