openwebui-cli/tests/test_chat_nonstreaming.py
2025-12-01 04:24:51 +01:00

900 lines
31 KiB
Python

"""Tests for non-streaming chat modes."""
import json
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
import httpx
import pytest
from typer.testing import CliRunner
from openwebui_cli.main import app
runner = CliRunner()
@pytest.fixture
def mock_config(tmp_path, monkeypatch):
"""Mock configuration for testing."""
config_dir = tmp_path / "openwebui"
config_path = config_dir / "config.yaml"
monkeypatch.setattr("openwebui_cli.config.get_config_dir", lambda: config_dir)
monkeypatch.setattr("openwebui_cli.config.get_config_path", lambda: config_path)
# Create default config
from openwebui_cli.config import Config, save_config
config = Config()
save_config(config)
return config_path
@pytest.fixture
def mock_keyring(monkeypatch):
"""Mock keyring for testing."""
token_store = {}
def get_password(service, key):
return token_store.get(f"{service}:{key}")
def set_password(service, key, password):
token_store[f"{service}:{key}"] = password
monkeypatch.setattr("keyring.get_password", get_password)
monkeypatch.setattr("keyring.set_password", set_password)
class TestNonStreamingJSON:
"""Tests for non-streaming mode with --json output."""
def test_nonstream_with_json_flag(self, mock_config, mock_keyring):
"""Test non-streaming response with --json flag."""
response_data = {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Complete response from model"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream", "--json"],
)
assert result.exit_code == 0
# Verify JSON output is printed
output = json.loads(result.stdout)
assert "choices" in output
assert output["choices"][0]["message"]["content"] == "Complete response from model"
def test_nonstream_json_with_multiple_fields(self, mock_config, mock_keyring):
"""Test that --json outputs complete response object."""
response_data = {
"id": "test-id-456",
"model": "gpt-4",
"choices": [
{
"message": {
"content": "Detailed response with metadata"
},
"finish_reason": "length"
}
],
"usage": {
"prompt_tokens": 50,
"completion_tokens": 100,
"total_tokens": 150
}
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "gpt-4", "-p", "Test", "--no-stream", "--json"],
)
assert result.exit_code == 0
output = json.loads(result.stdout)
assert output["id"] == "test-id-456"
assert output["usage"]["total_tokens"] == 150
assert output["choices"][0]["finish_reason"] == "length"
def test_nonstream_json_preserves_full_response(self, mock_config, mock_keyring):
"""Test that complete API response is returned with --json."""
response_data = {
"custom_field": "should_be_included",
"model": "test-model",
"choices": [
{
"message": {
"content": "Response content"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Test", "--no-stream", "--json"],
)
assert result.exit_code == 0
output = json.loads(result.stdout)
assert output["custom_field"] == "should_be_included"
class TestNonStreamingPlainText:
"""Tests for non-streaming mode without --json (plain text output)."""
def test_nonstream_plain_text_output(self, mock_config, mock_keyring):
"""Test non-streaming response outputs plain text content."""
response_data = {
"choices": [
{
"message": {
"content": "This is the plain text response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
# Without --json, only the content text should be printed
assert "This is the plain text response" in result.stdout
# Should NOT contain JSON structure
assert "{" not in result.stdout or result.stdout.count("{") == 0
def test_nonstream_plain_text_extracts_content_only(self, mock_config, mock_keyring):
"""Test that plain text mode extracts only message content."""
response_data = {
"id": "chatcmpl-789",
"model": "gpt-3.5",
"created": 1234567890,
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Just the content without metadata"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 20,
"completion_tokens": 30,
"total_tokens": 50
}
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "gpt-3.5", "-p", "Test", "--no-stream"],
)
assert result.exit_code == 0
# Only the content should appear
assert "Just the content without metadata" in result.stdout
# Metadata should NOT appear
assert "chatcmpl-789" not in result.stdout
assert "finish_reason" not in result.stdout
def test_nonstream_plain_text_multiline_response(self, mock_config, mock_keyring):
"""Test plain text output with multiline content."""
multiline_content = """This is a multiline response.
It contains multiple lines.
And some code:
```python
def hello():
print("world")
```
More text here."""
response_data = {
"choices": [
{
"message": {
"content": multiline_content
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Code request", "--no-stream"],
)
assert result.exit_code == 0
assert "multiline response" in result.stdout
assert "def hello():" in result.stdout
assert 'print("world")' in result.stdout
class TestNonStreamingEdgeCases:
"""Tests for non-streaming mode edge cases."""
def test_nonstream_empty_content(self, mock_config, mock_keyring):
"""Test non-streaming response with empty content."""
response_data = {
"choices": [
{
"message": {
"content": ""
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
def test_nonstream_missing_content_field(self, mock_config, mock_keyring):
"""Test non-streaming with missing content field."""
response_data = {
"choices": [
{
"message": {
"role": "assistant"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
def test_nonstream_special_characters_json(self, mock_config, mock_keyring):
"""Test non-streaming JSON output with special characters."""
response_data = {
"choices": [
{
"message": {
"content": "Response with special chars: é, ñ, 中文"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream", "--json"],
)
assert result.exit_code == 0
output = json.loads(result.stdout)
assert "é" in output["choices"][0]["message"]["content"]
assert "中文" in output["choices"][0]["message"]["content"]
def test_nonstream_json_with_newlines_in_content(self, mock_config, mock_keyring):
"""Test JSON output correctly handles newlines in content."""
response_data = {
"choices": [
{
"message": {
"content": "Line 1\nLine 2\nLine 3"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream", "--json"],
)
assert result.exit_code == 0
output = json.loads(result.stdout)
content = output["choices"][0]["message"]["content"]
assert "Line 1" in content
assert "Line 2" in content
assert "Line 3" in content
class TestNonStreamingWithOptions:
"""Tests for non-streaming mode with various command options."""
def test_nonstream_with_system_prompt(self, mock_config, mock_keyring):
"""Test non-streaming with system prompt."""
response_data = {
"choices": [
{
"message": {
"content": "Response respecting system prompt"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"-s", "You are a helpful assistant",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify system prompt was included in request
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
messages = request_body["messages"]
assert any(msg.get("role") == "system" for msg in messages)
def test_nonstream_with_temperature(self, mock_config, mock_keyring):
"""Test non-streaming with temperature parameter."""
response_data = {
"choices": [
{
"message": {
"content": "Creative response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Be creative",
"-T", "1.5",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify temperature was included in request
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
assert request_body["temperature"] == 1.5
def test_nonstream_with_max_tokens(self, mock_config, mock_keyring):
"""Test non-streaming with max-tokens parameter."""
response_data = {
"choices": [
{
"message": {
"content": "Limited response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Be brief",
"--max-tokens", "50",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify max_tokens was included in request
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
assert request_body["max_tokens"] == 50
def test_nonstream_with_chat_id(self, mock_config, mock_keyring):
"""Test non-streaming continuing an existing conversation."""
response_data = {
"choices": [
{
"message": {
"content": "Continuation response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Continue",
"--chat-id", "chat-abc-123",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify chat_id was included in request
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
assert request_body["chat_id"] == "chat-abc-123"
def test_nonstream_with_rag_context(self, mock_config, mock_keyring):
"""Test non-streaming with RAG file and collection context."""
response_data = {
"choices": [
{
"message": {
"content": "Response using RAG context"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Search my docs",
"--file", "file-123",
"--collection", "coll-456",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify files context was included
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
assert "files" in request_body
assert len(request_body["files"]) == 2
def test_nonstream_post_method_called(self, mock_config, mock_keyring):
"""Test that POST method is used for non-streaming (not stream)."""
response_data = {
"choices": [
{
"message": {
"content": "Test response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
# Verify post was called, not stream
mock_http_client.post.assert_called_once()
mock_http_client.stream.assert_not_called()
def test_nonstream_correct_endpoint(self, mock_config, mock_keyring):
"""Test that correct API endpoint is called."""
response_data = {
"choices": [
{
"message": {
"content": "Test response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
# Verify correct endpoint
mock_http_client.post.assert_called_once()
call_args = mock_http_client.post.call_args
assert call_args[0][0] == "/api/v1/chat/completions"
class TestNonStreamingWithHistory:
"""Tests for non-streaming mode with conversation history."""
def test_nonstream_with_history_file(self, tmp_path, mock_config, mock_keyring):
"""Test non-streaming with conversation history file."""
# Create history file
history_file = tmp_path / "history.json"
history = [
{"role": "user", "content": "What is 2+2?"},
{"role": "assistant", "content": "4"},
]
with open(history_file, "w") as f:
json.dump(history, f)
response_data = {
"choices": [
{
"message": {
"content": "Continuing conversation"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "What about 3+3?",
"--history-file", str(history_file),
"--no-stream"
],
)
assert result.exit_code == 0
# Verify history was included in request
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
messages = request_body["messages"]
assert len(messages) == 3 # 2 from history + 1 new user message
def test_nonstream_with_history_and_system_prompt(self, tmp_path, mock_config, mock_keyring):
"""Test non-streaming with history file and system prompt."""
history_file = tmp_path / "history.json"
history = [
{"role": "user", "content": "First question"},
{"role": "assistant", "content": "First answer"},
]
with open(history_file, "w") as f:
json.dump(history, f)
response_data = {
"choices": [
{
"message": {
"content": "Response with both history and system prompt"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Second question",
"-s", "You are a helpful assistant",
"--history-file", str(history_file),
"--no-stream",
"--json"
],
)
assert result.exit_code == 0
output = json.loads(result.stdout)
assert output["choices"][0]["message"]["content"]
class TestNonStreamingErrorHandling:
"""Tests for non-streaming mode error handling."""
def test_nonstream_with_stdin(self, mock_config, mock_keyring):
"""Test non-streaming with stdin input."""
response_data = {
"choices": [
{
"message": {
"content": "Response from stdin"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "--no-stream"],
input="Hello from stdin\n",
)
assert result.exit_code == 0
def test_nonstream_request_body_structure(self, mock_config, mock_keyring):
"""Test that request body has correct structure."""
response_data = {
"choices": [
{
"message": {
"content": "Test"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Hello", "--no-stream"],
)
assert result.exit_code == 0
# Verify request structure
call_args = mock_http_client.post.call_args
request_body = call_args.kwargs["json"]
assert "model" in request_body
assert "messages" in request_body
assert "stream" in request_body
assert request_body["stream"] is False