e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
510 lines
16 KiB
Python
510 lines
16 KiB
Python
"""Tests for the assistant-ui-sync-server-api client."""
|
|
|
|
import pytest
|
|
import httpx
|
|
import warnings
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
from assistant_ui import AssistantClient
|
|
from assistant_ui.types import Message, AssistantTransportCommand
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_basic_chat():
|
|
"""Test basic async chat functionality."""
|
|
client = AssistantClient(
|
|
base_url="https://api.example.com",
|
|
headers={"Authorization": "Bearer test"}
|
|
)
|
|
|
|
messages: list[Message] = [
|
|
{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": "Hello"}],
|
|
}
|
|
]
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"response": "Hi there!"}
|
|
mock_response.text = "Success"
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-123")
|
|
|
|
# Suppress deprecation warning for this test
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", DeprecationWarning)
|
|
response = await thread.chat(
|
|
messages=messages,
|
|
system="You are helpful"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Check the request was made correctly
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["threadId"] == "thread-123"
|
|
assert payload["messages"] == messages
|
|
assert payload["system"] == "You are helpful"
|
|
# Commands should also be present due to conversion
|
|
assert "commands" in payload
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chat_with_tools():
|
|
"""Test chat with tools."""
|
|
async with AssistantClient(base_url="https://api.example.com") as client:
|
|
messages: list[Message] = []
|
|
tools = {"search": {"description": "Search the web"}}
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-456")
|
|
await thread.chat(messages=messages, tools=tools)
|
|
|
|
# Check tools were included
|
|
call_args = mock_client.request.call_args
|
|
assert call_args[1]["json"]["tools"] == tools
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel():
|
|
"""Test cancel functionality."""
|
|
client = AssistantClient(base_url="https://api.example.com")
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-789")
|
|
await thread.cancel()
|
|
|
|
# Check cancel request was made
|
|
mock_client.request.assert_called_once_with(
|
|
"POST",
|
|
"/api/cancel",
|
|
headers={"Content-Type": "application/json"},
|
|
json={"threadId": "thread-789"}
|
|
)
|
|
|
|
await client.close()
|
|
|
|
|
|
def test_sync_chat():
|
|
"""Test synchronous chat."""
|
|
with AssistantClient(base_url="https://api.example.com") as client:
|
|
messages: list[Message] = [
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "text", "text": "I can help."},
|
|
{
|
|
"type": "tool-call",
|
|
"toolCallId": "call-123",
|
|
"toolName": "search",
|
|
"args": {"query": "weather"}
|
|
}
|
|
],
|
|
}
|
|
]
|
|
|
|
mock_response = Mock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
mock_response.text = "Success"
|
|
|
|
with patch.object(client, "_ensure_sync_client") as mock_ensure:
|
|
mock_client = Mock()
|
|
mock_client.request = Mock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-sync")
|
|
response = thread.chat_sync(messages=messages)
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_headers():
|
|
"""Test with async header function."""
|
|
async def get_headers():
|
|
return {"Authorization": "Bearer dynamic-token"}
|
|
|
|
client = AssistantClient(
|
|
base_url="https://api.example.com",
|
|
headers=get_headers
|
|
)
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-async-headers")
|
|
await thread.chat(messages=[])
|
|
|
|
# Check dynamic headers were used
|
|
call_args = mock_client.request.call_args
|
|
assert call_args[1]["headers"]["Authorization"] == "Bearer dynamic-token"
|
|
|
|
await client.close()
|
|
|
|
|
|
def test_sync_with_async_headers_raises():
|
|
"""Test that sync methods raise error with async headers."""
|
|
async def get_headers():
|
|
return {"Authorization": "Bearer token"}
|
|
|
|
client = AssistantClient(
|
|
base_url="https://api.example.com",
|
|
headers=get_headers
|
|
)
|
|
|
|
thread = client.threads("thread-123")
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
thread.chat_sync(messages=[])
|
|
|
|
assert "async header functions" in str(exc_info.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_handling():
|
|
"""Test error handling."""
|
|
client = AssistantClient(base_url="https://api.example.com")
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = False
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal Server Error"
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-error")
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
await thread.chat(messages=[])
|
|
|
|
assert "Request failed with status 500" in str(exc_info.value)
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_context_manager():
|
|
"""Test async context manager."""
|
|
async with AssistantClient(base_url="https://api.example.com") as client:
|
|
assert client._async_client is None # Not created until first use
|
|
|
|
# Force client creation
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
|
|
with patch("httpx.AsyncClient") as mock_async_client_class:
|
|
mock_instance = AsyncMock()
|
|
mock_instance.request = AsyncMock(return_value=mock_response)
|
|
mock_async_client_class.return_value = mock_instance
|
|
|
|
thread = client.threads("test")
|
|
await thread.chat(messages=[])
|
|
|
|
assert client._async_client is not None
|
|
|
|
# After context exit, client should be closed
|
|
assert client._async_client is None
|
|
|
|
|
|
def test_sync_context_manager():
|
|
"""Test sync context manager."""
|
|
with AssistantClient(base_url="https://api.example.com") as client:
|
|
assert client._sync_client is None # Not created until first use
|
|
|
|
# Force client creation
|
|
mock_response = Mock()
|
|
mock_response.is_success = True
|
|
|
|
with patch("httpx.Client") as mock_client_class:
|
|
mock_instance = Mock()
|
|
mock_instance.request = Mock(return_value=mock_response)
|
|
mock_client_class.return_value = mock_instance
|
|
|
|
thread = client.threads("test")
|
|
thread.chat_sync(messages=[])
|
|
|
|
assert client._sync_client is not None
|
|
|
|
# After context exit, client should be closed
|
|
assert client._sync_client is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_parameters():
|
|
"""Test chat with all possible parameters."""
|
|
client = AssistantClient(
|
|
base_url="https://api.example.com",
|
|
headers={"X-API-Key": "test-key"},
|
|
timeout=30.0
|
|
)
|
|
|
|
messages: list[Message] = [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "Analyze this:"},
|
|
{"type": "image", "image": "https://example.com/img.jpg"},
|
|
{"type": "file", "data": "https://example.com/doc.pdf", "mimeType": "application/pdf"}
|
|
],
|
|
"unstable_id": "msg-123",
|
|
}
|
|
]
|
|
|
|
tools = {
|
|
"analyze": {
|
|
"description": "Analyze content",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"content": {"type": "string"}},
|
|
"required": ["content"]
|
|
}
|
|
}
|
|
}
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-all-params")
|
|
await thread.chat(
|
|
messages=messages,
|
|
tools=tools,
|
|
system="You are an analyzer",
|
|
unstable_assistantMessageId="asst-456",
|
|
runConfig={"model": "gpt-5.4-nano"},
|
|
state={"session": "abc"},
|
|
custom_field="custom_value"
|
|
)
|
|
|
|
# Check all parameters were included
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["threadId"] == "thread-all-params"
|
|
assert payload["system"] == "You are an analyzer"
|
|
assert payload["messages"] == messages
|
|
assert payload["tools"] == tools
|
|
assert payload["unstable_assistantMessageId"] == "asst-456"
|
|
assert payload["runConfig"] == {"model": "gpt-5.4-nano"}
|
|
assert payload["state"] == {"session": "abc"}
|
|
assert payload["custom_field"] == "custom_value"
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chat_with_commands():
|
|
"""Test chat with commands instead of messages."""
|
|
client = AssistantClient(
|
|
base_url="https://api.example.com",
|
|
headers={"Authorization": "Bearer test"}
|
|
)
|
|
|
|
commands: list[AssistantTransportCommand] = [
|
|
{
|
|
"type": "add-message",
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [{"type": "text", "text": "Hello with commands"}]
|
|
}
|
|
},
|
|
{
|
|
"type": "add-tool-result",
|
|
"toolCallId": "call-789",
|
|
"toolName": "search",
|
|
"result": {"data": "search results"},
|
|
"isError": False,
|
|
"artifact": None
|
|
}
|
|
]
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-cmd")
|
|
response = await thread.chat(commands=commands)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Check the request was made with commands
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["threadId"] == "thread-cmd"
|
|
assert payload["commands"] == commands
|
|
assert "messages" not in payload
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chat_with_deprecated_messages():
|
|
"""Test that messages parameter shows deprecation warning."""
|
|
client = AssistantClient(base_url="https://api.example.com")
|
|
|
|
messages: list[Message] = [
|
|
{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": "Using deprecated API"}],
|
|
}
|
|
]
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-deprecated")
|
|
|
|
# Check deprecation warning
|
|
with warnings.catch_warnings(record=True) as w:
|
|
warnings.simplefilter("always")
|
|
await thread.chat(messages=messages)
|
|
|
|
assert len(w) == 1
|
|
assert issubclass(w[0].category, DeprecationWarning)
|
|
assert "messages' parameter is deprecated" in str(w[0].message)
|
|
|
|
# Check both messages and commands were sent
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["messages"] == messages
|
|
assert "commands" in payload
|
|
assert len(payload["commands"]) > 0
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chat_messages_and_commands_together():
|
|
"""Test using both messages and commands together."""
|
|
client = AssistantClient(base_url="https://api.example.com")
|
|
|
|
messages: list[Message] = [
|
|
{
|
|
"role": "assistant",
|
|
"content": [{"type": "text", "text": "Previous message"}],
|
|
}
|
|
]
|
|
|
|
commands: list[AssistantTransportCommand] = [
|
|
{
|
|
"type": "add-message",
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [{"type": "text", "text": "New command"}]
|
|
}
|
|
}
|
|
]
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_async_client") as mock_ensure:
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-both")
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
await thread.chat(messages=messages, commands=commands)
|
|
|
|
# Check both were merged
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["messages"] == messages
|
|
assert "commands" in payload
|
|
# Should have original command plus converted message
|
|
assert len(payload["commands"]) >= 2
|
|
|
|
await client.close()
|
|
|
|
|
|
def test_sync_chat_with_commands():
|
|
"""Test synchronous chat with commands."""
|
|
with AssistantClient(base_url="https://api.example.com") as client:
|
|
commands: list[AssistantTransportCommand] = [
|
|
{
|
|
"type": "add-message",
|
|
"message": {
|
|
"role": "assistant",
|
|
"parts": [{"type": "text", "text": "Sync command test"}]
|
|
}
|
|
}
|
|
]
|
|
|
|
mock_response = Mock()
|
|
mock_response.is_success = True
|
|
mock_response.status_code = 200
|
|
|
|
with patch.object(client, "_ensure_sync_client") as mock_ensure:
|
|
mock_client = Mock()
|
|
mock_client.request = Mock(return_value=mock_response)
|
|
mock_ensure.return_value = mock_client
|
|
|
|
thread = client.threads("thread-sync-cmd")
|
|
response = thread.chat_sync(commands=commands)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Check commands were sent
|
|
call_args = mock_client.request.call_args
|
|
payload = call_args[1]["json"]
|
|
|
|
assert payload["commands"] == commands |