confluence_web/tests/backend/test_model.py
Artur Mukhamadiev e65fbf4b67 backend: FastAPI backend track handoff (contract revision 1)
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.
2026-09-14 21:57:54 +03:00

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()