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

320 lines
11 KiB
Python

"""Tests for chat command error conditions with missing parameters."""
import sys
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
import pytest
from typer.testing import CliRunner
from openwebui_cli.main import app
from openwebui_cli.config import Config
runner = CliRunner()
@pytest.fixture
def mock_config(tmp_path, monkeypatch):
"""Mock configuration for testing (no default model)."""
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 with no default model
from openwebui_cli.config import Config, save_config
config = Config()
config.defaults.model = None # Explicitly no default model
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 TestMissingModel:
"""Test cases for missing model parameter."""
def test_missing_model_no_default(self, mock_config, mock_keyring):
"""Test chat fails when model is not specified and no default configured."""
result = runner.invoke(
app,
["chat", "send", "-p", "Hello world"],
)
assert result.exit_code == 2
assert "model" in result.stdout.lower()
assert "error" in result.stdout.lower()
def test_missing_model_error_message_content(self, mock_config, mock_keyring):
"""Test that error message provides helpful guidance."""
result = runner.invoke(
app,
["chat", "send", "-p", "Hello"],
)
assert result.exit_code == 2
# Should mention both the missing model and how to fix it
assert "model" in result.stdout.lower()
assert any(
keyword in result.stdout.lower()
for keyword in ["default", "config", "specify"]
)
def test_missing_model_short_flag(self, mock_config, mock_keyring):
"""Test missing model error with short flag usage."""
result = runner.invoke(
app,
["chat", "send", "-p", "Test prompt"],
)
assert result.exit_code == 2
assert "model" in result.stdout.lower()
class TestMissingPromptHandling:
"""Test cases for missing prompt with various input conditions."""
def test_missing_prompt_with_no_stdin_input(self, mock_config, mock_keyring):
"""Test chat fails when prompt is missing and no stdin input provided."""
# When no input parameter is passed to runner.invoke(),
# and no -p flag provided, should attempt to read stdin
# and get empty, which could cause issues
result = runner.invoke(
app,
["--token", "test-token", "chat", "send", "-m", "test-model"],
)
# Without mocking HTTP, this will hit auth/connection errors
# But the prompt validation should happen before that
# Exit code could be 1 (network) or 2 (usage) depending on implementation
assert result.exit_code in [1, 2]
def test_missing_prompt_with_valid_stdin_input(self, mock_config, mock_keyring):
"""Test that valid stdin input is accepted for prompt."""
response_data = {
"choices": [
{
"message": {
"content": "Response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_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_client.return_value = mock_http_client
# Provide valid stdin input when -p not used
result = runner.invoke(
app,
["--token", "test-token", "chat", "send", "-m", "test-model", "--no-stream"],
input="Valid prompt from stdin\n",
)
# Should succeed with valid prompt provided via stdin
assert result.exit_code == 0
class TestMissingPromptWithStdin:
"""Test cases for prompt handling with stdin."""
def test_prompt_from_stdin_overrides_missing_prompt_flag(self, mock_config, mock_keyring):
"""Test that stdin input works when -p flag is not provided."""
response_data = {
"choices": [
{
"message": {
"content": "Response from stdin"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_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_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
class TestBothParametersMissing:
"""Test cases for both model and prompt missing."""
def test_missing_both_model_and_prompt(self, mock_config, mock_keyring):
"""Test error when both model and prompt are missing."""
result = runner.invoke(
app,
["chat", "send"],
)
# Should fail with exit code 2
assert result.exit_code == 2
# Should mention one of the missing parameters
output_lower = result.stdout.lower()
assert "error" in output_lower
def test_missing_both_shows_model_error_first(self, mock_config, mock_keyring):
"""Test that missing model is caught first when both are missing."""
result = runner.invoke(
app,
["chat", "send"],
)
assert result.exit_code == 2
# Model check happens first in the code
assert "model" in result.stdout.lower()
class TestParameterValidation:
"""Test comprehensive parameter validation."""
def test_model_required_with_valid_prompt(self, mock_config, mock_keyring):
"""Test that model is required even with valid prompt."""
result = runner.invoke(
app,
["chat", "send", "-p", "Valid prompt here"],
)
assert result.exit_code == 2
assert "model" in result.stdout.lower()
def test_prompt_required_with_valid_model(self, mock_config, mock_keyring):
"""Test that prompt is required even with valid model."""
response_data = {"choices": [{"message": {"content": "Response"}}]}
with patch("openwebui_cli.commands.chat.create_client") as mock_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_client.return_value = mock_http_client
result = runner.invoke(
app,
["--token", "test-token", "chat", "send", "-m", "valid-model", "--no-stream"],
input="test input",
)
assert result.exit_code == 0
def test_both_parameters_success(self, mock_config, mock_keyring):
"""Test successful invocation with both parameters provided."""
response_data = {
"choices": [
{
"message": {
"content": "Success response"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_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_client.return_value = mock_http_client
result = runner.invoke(
app,
["chat", "send", "-m", "test-model", "-p", "Test prompt", "--no-stream"],
)
assert result.exit_code == 0
class TestExitCodes:
"""Test that error conditions return correct exit codes."""
def test_missing_model_exit_code_is_2(self, mock_config, mock_keyring):
"""Test exit code 2 for missing model (usage error)."""
result = runner.invoke(
app,
["chat", "send", "-p", "Test"],
)
assert result.exit_code == 2
def test_missing_prompt_with_stdin_input(self, mock_config, mock_keyring):
"""Test that stdin input can be used when -p flag not provided."""
response_data = {
"choices": [
{
"message": {
"content": "Response from stdin input"
}
}
]
}
with patch("openwebui_cli.commands.chat.create_client") as mock_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_client.return_value = mock_http_client
result = runner.invoke(
app,
["--token", "test-token", "chat", "send", "-m", "test-model", "--no-stream"],
input="Prompt from stdin",
)
# Should succeed because prompt is provided via stdin
assert result.exit_code == 0
def test_exit_code_not_1(self, mock_config, mock_keyring):
"""Test that parameter errors are not generic exit code 1."""
result = runner.invoke(
app,
["chat", "send"],
)
# Should be 2 (usage error), not 1 (general error)
assert result.exit_code == 2