1067 lines
36 KiB
Python
1067 lines
36 KiB
Python
"""Tests for RAG commands."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from openwebui_cli.errors import ServerError
|
|
from openwebui_cli.main import app
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_config(tmp_path, monkeypatch):
|
|
"""Isolate config writes."""
|
|
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)
|
|
|
|
from openwebui_cli.config import Config, save_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)
|
|
|
|
|
|
def _mock_client(get_data=None, post_data=None, delete_data=None, status_code=200):
|
|
"""Helper to create mock HTTP client."""
|
|
client = MagicMock()
|
|
client.__enter__.return_value = client
|
|
client.__exit__.return_value = None
|
|
|
|
if get_data is not None:
|
|
get_response = Mock()
|
|
get_response.status_code = status_code
|
|
get_response.json.return_value = get_data
|
|
client.get.return_value = get_response
|
|
|
|
if post_data is not None:
|
|
post_response = Mock()
|
|
post_response.status_code = status_code
|
|
post_response.json.return_value = post_data
|
|
client.post.return_value = post_response
|
|
|
|
if delete_data is not None:
|
|
delete_response = Mock()
|
|
delete_response.status_code = status_code
|
|
delete_response.json.return_value = delete_data
|
|
client.delete.return_value = delete_response
|
|
|
|
return client
|
|
|
|
|
|
# ===== Files Commands =====
|
|
|
|
def test_files_list_success(mock_keyring):
|
|
"""Test listing files successfully."""
|
|
files = [
|
|
{"id": "file-1", "filename": "doc.pdf", "size": 2048},
|
|
{"id": "file-2", "name": "report.txt", "size": 1024},
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=files)
|
|
|
|
result = runner.invoke(app, ["rag", "files", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "doc.pdf" in result.stdout
|
|
assert "report.txt" in result.stdout
|
|
assert "2.0 KB" in result.stdout
|
|
assert "1.0 KB" in result.stdout
|
|
|
|
|
|
def test_files_list_empty(mock_keyring):
|
|
"""Test listing files when none exist."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=[])
|
|
|
|
result = runner.invoke(app, ["rag", "files", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No files found" in result.stdout
|
|
|
|
|
|
def test_files_list_json_format(mock_keyring):
|
|
"""Test listing files in JSON format."""
|
|
files = [{"id": "file-1", "filename": "doc.pdf", "size": 2048}]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=files)
|
|
|
|
result = runner.invoke(app, ["--format", "json", "rag", "files", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
output = json.loads(result.stdout)
|
|
assert isinstance(output, list)
|
|
assert output[0]["id"] == "file-1"
|
|
|
|
|
|
def test_files_list_wrapped_response(mock_keyring):
|
|
"""Test listing files when API wraps response in object."""
|
|
response = {"files": [{"id": "file-1", "filename": "doc.pdf", "size": 2048}]}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=response)
|
|
|
|
result = runner.invoke(app, ["rag", "files", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "doc.pdf" in result.stdout
|
|
|
|
|
|
def test_files_upload_success(tmp_path, mock_keyring):
|
|
"""Test uploading a file successfully."""
|
|
test_file = tmp_path / "test.pdf"
|
|
test_file.write_text("test content")
|
|
|
|
response = {"id": "file-123", "filename": "test.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "upload", str(test_file)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploaded: test.pdf" in result.stdout
|
|
assert "file-123" in result.stdout
|
|
# Verify the client was created with extended timeout
|
|
mock_client_factory.assert_called_once()
|
|
call_kwargs = mock_client_factory.call_args.kwargs
|
|
assert call_kwargs.get("timeout") == 300
|
|
|
|
|
|
def test_files_upload_missing_file(mock_keyring):
|
|
"""Test uploading a non-existent file."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "upload", "/nonexistent/file.pdf"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Error: File not found: /nonexistent/file.pdf" in result.stdout
|
|
assert "Summary: 0 successful, 1 failed" in result.stdout
|
|
|
|
|
|
def test_files_upload_multiple_files(tmp_path, mock_keyring):
|
|
"""Test uploading multiple files."""
|
|
file1 = tmp_path / "doc1.pdf"
|
|
file2 = tmp_path / "doc2.pdf"
|
|
file1.write_text("content1")
|
|
file2.write_text("content2")
|
|
|
|
response1 = {"id": "file-1", "filename": "doc1.pdf"}
|
|
response2 = {"id": "file-2", "filename": "doc2.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
|
|
# Mock multiple post responses
|
|
mock_client.post.side_effect = [
|
|
Mock(status_code=200, json=lambda: response1),
|
|
Mock(status_code=200, json=lambda: response2),
|
|
]
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "upload", str(file1), str(file2)]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploaded: doc1.pdf" in result.stdout
|
|
assert "Uploaded: doc2.pdf" in result.stdout
|
|
assert mock_client.post.call_count == 2
|
|
|
|
|
|
def test_files_upload_with_collection(tmp_path, mock_keyring):
|
|
"""Test uploading a file and adding to collection."""
|
|
test_file = tmp_path / "test.pdf"
|
|
test_file.write_text("test content")
|
|
|
|
upload_response = {"id": "file-123", "filename": "test.pdf"}
|
|
add_response = {}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
|
|
# First post for upload, second post for adding to collection
|
|
mock_client.post.side_effect = [
|
|
Mock(status_code=200, json=lambda: upload_response),
|
|
Mock(status_code=200, json=lambda: add_response),
|
|
]
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "files", "upload", str(test_file), "--collection", "coll-456"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploaded: test.pdf" in result.stdout
|
|
assert "Added to collection: coll-456" in result.stdout
|
|
# Verify both API calls were made
|
|
assert mock_client.post.call_count == 2
|
|
|
|
|
|
def test_files_upload_collection_failure_continues(tmp_path, mock_keyring):
|
|
"""Test that collection add failure doesn't block upload."""
|
|
test_file = tmp_path / "test.pdf"
|
|
test_file.write_text("test content")
|
|
|
|
upload_response = {"id": "file-123", "filename": "test.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
|
|
# First post succeeds, second raises exception
|
|
mock_client.post.side_effect = [
|
|
Mock(status_code=200, json=lambda: upload_response),
|
|
Exception("Collection not found"),
|
|
]
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "files", "upload", str(test_file), "--collection", "invalid"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploaded: test.pdf" in result.stdout
|
|
assert "Error: Could not add to collection" in result.stdout
|
|
assert "Summary: 1 successful, 0 failed" in result.stdout
|
|
|
|
|
|
def test_files_upload_unknown_id(tmp_path, mock_keyring):
|
|
"""Test uploading when response has no ID."""
|
|
test_file = tmp_path / "test.pdf"
|
|
test_file.write_text("test content")
|
|
|
|
response = {"filename": "test.pdf"} # No ID
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "upload", str(test_file)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Warning: Upload succeeded but got no file ID" in result.stdout
|
|
assert "Summary: 0 successful, 1 failed" in result.stdout
|
|
|
|
|
|
def test_files_delete_with_confirmation(mock_keyring):
|
|
"""Test deleting a file with user confirmation."""
|
|
response = {}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(delete_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "delete", "file-123"], input="y\n"
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Deleted file: file-123" in result.stdout
|
|
mock_client.delete.assert_called_once_with("/api/v1/files/file-123")
|
|
|
|
|
|
def test_files_delete_skip_confirmation(mock_keyring):
|
|
"""Test deleting a file with force flag."""
|
|
response = {}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(delete_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "delete", "file-123", "--force"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Deleted file: file-123" in result.stdout
|
|
|
|
|
|
def test_files_delete_abort(mock_keyring):
|
|
"""Test aborting file deletion."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "delete", "file-123"], input="n\n"
|
|
)
|
|
|
|
assert result.exit_code == 1 # Abort returns exit code 1
|
|
|
|
|
|
# ===== Collections Commands =====
|
|
|
|
def test_collections_list_success(mock_keyring):
|
|
"""Test listing collections successfully."""
|
|
collections = [
|
|
{"id": "coll-1", "name": "Documents", "description": "All documents"},
|
|
{"id": "coll-2", "name": "Articles", "description": "Research articles"},
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=collections)
|
|
|
|
result = runner.invoke(app, ["rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Documents" in result.stdout
|
|
assert "Articles" in result.stdout
|
|
assert "All documents" in result.stdout
|
|
|
|
|
|
def test_collections_list_empty(mock_keyring):
|
|
"""Test listing collections when none exist."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=[])
|
|
|
|
result = runner.invoke(app, ["rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No collections found" in result.stdout
|
|
|
|
|
|
def test_collections_list_json_format(mock_keyring):
|
|
"""Test listing collections in JSON format."""
|
|
collections = [
|
|
{"id": "coll-1", "name": "Documents", "description": "All documents"}
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=collections)
|
|
|
|
result = runner.invoke(app, ["--format", "json", "rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
output = json.loads(result.stdout)
|
|
assert isinstance(output, list)
|
|
assert output[0]["name"] == "Documents"
|
|
|
|
|
|
def test_collections_list_wrapped_response(mock_keyring):
|
|
"""Test listing collections when API wraps response in object."""
|
|
response = {
|
|
"collections": [{"id": "coll-1", "name": "Documents", "description": "Test"}]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=response)
|
|
|
|
result = runner.invoke(app, ["rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Documents" in result.stdout
|
|
|
|
|
|
def test_collections_list_truncated_description(mock_keyring):
|
|
"""Test that long descriptions are truncated."""
|
|
collections = [
|
|
{
|
|
"id": "coll-1",
|
|
"name": "Docs",
|
|
"description": "A" * 100, # Very long description
|
|
}
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=collections)
|
|
|
|
result = runner.invoke(app, ["rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
# Description should be truncated to 50 chars
|
|
assert "A" * 50 in result.stdout or result.stdout.count("A") <= 50
|
|
|
|
|
|
def test_collections_create_success(mock_keyring):
|
|
"""Test creating a collection successfully."""
|
|
response = {"id": "coll-789", "name": "New Collection"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "collections", "create", "New Collection"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Created collection: New Collection" in result.stdout
|
|
assert "coll-789" in result.stdout
|
|
|
|
# Verify the request was made correctly
|
|
mock_client.post.assert_called_once_with(
|
|
"/api/v1/knowledge/",
|
|
json={"name": "New Collection", "description": ""},
|
|
)
|
|
|
|
|
|
def test_collections_create_with_description(mock_keyring):
|
|
"""Test creating a collection with description."""
|
|
response = {"id": "coll-789", "name": "New Collection"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"rag",
|
|
"collections",
|
|
"create",
|
|
"New Collection",
|
|
"--description",
|
|
"Test description",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Created collection: New Collection" in result.stdout
|
|
|
|
# Verify the request includes description
|
|
mock_client.post.assert_called_once_with(
|
|
"/api/v1/knowledge/",
|
|
json={"name": "New Collection", "description": "Test description"},
|
|
)
|
|
|
|
|
|
def test_collections_create_no_id_response(mock_keyring):
|
|
"""Test creating collection when response has no ID."""
|
|
response = {"name": "New Collection"} # No ID
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "collections", "create", "New Collection"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Warning: Collection created but got no ID" in result.stdout
|
|
|
|
|
|
def test_collections_delete_with_confirmation(mock_keyring):
|
|
"""Test deleting a collection with user confirmation."""
|
|
response = {}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(delete_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "collections", "delete", "coll-456"], input="y\n"
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Deleted collection: coll-456" in result.stdout
|
|
mock_client.delete.assert_called_once_with("/api/v1/knowledge/coll-456")
|
|
|
|
|
|
def test_collections_delete_skip_confirmation(mock_keyring):
|
|
"""Test deleting a collection with force flag."""
|
|
response = {}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(delete_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "collections", "delete", "coll-456", "--force"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Deleted collection: coll-456" in result.stdout
|
|
|
|
|
|
def test_collections_delete_abort(mock_keyring):
|
|
"""Test aborting collection deletion."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "collections", "delete", "coll-456"], input="n\n"
|
|
)
|
|
|
|
assert result.exit_code == 0 # Returns normally with cancellation message
|
|
assert "Delete cancelled" in result.stdout
|
|
|
|
|
|
# ===== Search Command =====
|
|
|
|
def test_search_success(mock_keyring):
|
|
"""Test searching in a collection successfully."""
|
|
results = {
|
|
"results": [
|
|
{"content": "Hello world", "score": 0.95},
|
|
{"content": "Hello there", "score": 0.87},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "hello", "--collection", "coll-1", "--top-k", "2"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Search results for: hello" in result.stdout
|
|
assert "Hello world" in result.stdout
|
|
assert "Hello there" in result.stdout
|
|
assert "0.95" in result.stdout
|
|
|
|
# Verify the request
|
|
mock_client.post.assert_called_once_with(
|
|
"/api/v1/knowledge/coll-1/query",
|
|
json={"query": "hello", "k": 2},
|
|
)
|
|
|
|
|
|
def test_search_default_top_k(mock_keyring):
|
|
"""Test search with default top_k value."""
|
|
results = {"results": [{"content": "Result", "score": 0.9}]}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
# Verify default k=5 was used
|
|
call_args = mock_client.post.call_args
|
|
assert call_args.kwargs["json"]["k"] == 5
|
|
|
|
|
|
def test_search_no_results(mock_keyring):
|
|
"""Test search with no results."""
|
|
results = {"results": []}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "nosuchterm", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "No results found for query" in result.stdout
|
|
|
|
|
|
def test_search_documents_fallback(mock_keyring):
|
|
"""Test search when response uses 'documents' key instead of 'results'."""
|
|
results = {
|
|
"documents": [
|
|
{"text": "Document content", "distance": 0.05},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Document content" in result.stdout
|
|
|
|
|
|
def test_search_json_format(mock_keyring):
|
|
"""Test search with JSON output format."""
|
|
results = {
|
|
"results": [
|
|
{"content": "Test result", "score": 0.92},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"--format",
|
|
"json",
|
|
"rag",
|
|
"search",
|
|
"query",
|
|
"--collection",
|
|
"coll-1",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
output = json.loads(result.stdout)
|
|
assert isinstance(output, list)
|
|
assert output[0]["content"] == "Test result"
|
|
|
|
|
|
def test_search_long_content_truncated(mock_keyring):
|
|
"""Test that long search result content is truncated."""
|
|
long_text = "A" * 500
|
|
results = {
|
|
"results": [
|
|
{"content": long_text, "score": 0.9},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
# Content should be truncated to 200 chars
|
|
assert result.stdout.count("A") <= 200
|
|
|
|
|
|
def test_search_with_short_option(mock_keyring):
|
|
"""Test search using short option flags."""
|
|
results = {"results": [{"content": "Result", "score": 0.9}]}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "-c", "coll-1", "-k", "3"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
call_args = mock_client.post.call_args
|
|
assert call_args.kwargs["json"]["k"] == 3
|
|
|
|
|
|
# ===== Error Handling =====
|
|
|
|
def test_files_list_api_error(mock_keyring):
|
|
"""Test handling API error when listing files."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
mock_client.get.side_effect = ServerError("API error")
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "list"])
|
|
|
|
# Should handle error gracefully
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_collections_create_api_error(mock_keyring):
|
|
"""Test handling API error when creating collection."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
mock_client.post.side_effect = ServerError("API error")
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "collections", "create", "Test"]
|
|
)
|
|
|
|
# Should handle error gracefully
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_api_error(mock_keyring):
|
|
"""Test handling API error when searching."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
mock_client.post.side_effect = ServerError("Collection not found")
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "invalid"],
|
|
)
|
|
|
|
# Should handle error gracefully
|
|
assert result.exit_code != 0
|
|
|
|
|
|
# ===== Edge Cases =====
|
|
|
|
def test_files_list_missing_fields(mock_keyring):
|
|
"""Test listing files when response has missing fields."""
|
|
files = [
|
|
{"id": "file-1"}, # Missing filename and size
|
|
{"filename": "doc.pdf"}, # Missing id and size
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=files)
|
|
|
|
result = runner.invoke(app, ["rag", "files", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "-" in result.stdout # Missing fields should show as "-"
|
|
|
|
|
|
def test_collections_list_missing_fields(mock_keyring):
|
|
"""Test listing collections when response has missing fields."""
|
|
collections = [
|
|
{"id": "coll-1"}, # Missing name and description
|
|
{"name": "Docs"}, # Missing id and description
|
|
]
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client_factory.return_value = _mock_client(get_data=collections)
|
|
|
|
result = runner.invoke(app, ["rag", "collections", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "-" in result.stdout # Missing fields should show as "-"
|
|
|
|
|
|
def test_search_with_metadata(mock_keyring):
|
|
"""Test search results with metadata information."""
|
|
results = {
|
|
"results": [
|
|
{
|
|
"content": "Result with metadata",
|
|
"score": 0.95,
|
|
"metadata": {"source": "test.pdf"},
|
|
}
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Result with metadata" in result.stdout
|
|
assert "Source: test.pdf" in result.stdout
|
|
|
|
|
|
def test_files_upload_with_missing_collection_id(tmp_path, mock_keyring):
|
|
"""Test that upload continues even if file ID is missing."""
|
|
test_file = tmp_path / "test.pdf"
|
|
test_file.write_text("test content")
|
|
|
|
# API returns no ID
|
|
upload_response = {"filename": "test.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=upload_response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "files", "upload", str(test_file), "--collection", "coll-456"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Warning: Upload succeeded but got no file ID" in result.stdout
|
|
# Collection add should be skipped since file_id == "unknown"
|
|
assert mock_client.post.call_count == 1 # Only upload attempt, no collection add
|
|
assert "Summary: 0 successful, 1 failed" in result.stdout
|
|
|
|
|
|
# ===== Additional Coverage Tests =====
|
|
|
|
def test_files_upload_large_file_warning(tmp_path, mock_keyring):
|
|
"""Test large file warning is shown."""
|
|
test_file = tmp_path / "large.pdf"
|
|
# Create file > 10MB (11 MB)
|
|
test_file.write_bytes(b"x" * (11 * 1024 * 1024))
|
|
|
|
response = {"id": "file-123", "filename": "large.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=response)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "upload", str(test_file)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploading: large.pdf" in result.stdout
|
|
|
|
|
|
def test_files_upload_not_a_file(tmp_path, mock_keyring):
|
|
"""Test uploading when path is not a file."""
|
|
directory = tmp_path / "notafile"
|
|
directory.mkdir()
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(app, ["rag", "files", "upload", str(directory)])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Error: Not a file:" in result.stdout
|
|
assert "Summary: 0 successful, 1 failed" in result.stdout
|
|
|
|
|
|
def test_files_delete_confirmation_abort(mock_keyring):
|
|
"""Test aborting file deletion via confirmation."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "delete", "file-123"], input="n\n"
|
|
)
|
|
|
|
assert result.exit_code == 1 # Abort returns exit code 1
|
|
|
|
|
|
def test_search_validation_empty_query(mock_keyring):
|
|
"""Test search rejects empty query."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_validation_short_query(mock_keyring):
|
|
"""Test search rejects too short query."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "ab", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_validation_missing_collection(mock_keyring):
|
|
"""Test search requires collection ID."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_validation_invalid_top_k(mock_keyring):
|
|
"""Test search validation of top_k parameter."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1", "--top-k", "0"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_validation_high_top_k_warning(mock_keyring):
|
|
"""Test search warning for high top_k value."""
|
|
results = {"results": []}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1", "--top-k", "101"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Warning: Requesting more than 100 results" in result.stdout
|
|
|
|
|
|
def test_collections_create_empty_name_validation(mock_keyring):
|
|
"""Test collection creation rejects empty name."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "collections", "create", ""],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_files_delete_empty_id_validation(mock_keyring):
|
|
"""Test file deletion rejects empty ID."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "files", "delete", "", "--force"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_collections_delete_empty_id_validation(mock_keyring):
|
|
"""Test collection deletion rejects empty ID."""
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "collections", "delete", "", "--force"],
|
|
)
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
def test_search_result_with_text_field(mock_keyring):
|
|
"""Test search result using 'text' field instead of 'content'."""
|
|
results = {
|
|
"results": [
|
|
{"text": "Text field result", "distance": 0.1},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Text field result" in result.stdout
|
|
|
|
|
|
def test_search_result_with_distance_field(mock_keyring):
|
|
"""Test search result using 'distance' instead of 'score'."""
|
|
results = {
|
|
"results": [
|
|
{"content": "Result", "distance": 0.05},
|
|
]
|
|
}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = _mock_client(post_data=results)
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["rag", "search", "query", "--collection", "coll-1"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "0.05" in result.stdout
|
|
|
|
|
|
def test_files_upload_multiple_with_mixed_success(tmp_path, mock_keyring):
|
|
"""Test uploading multiple files with mixed success/failure."""
|
|
file1 = tmp_path / "success.pdf"
|
|
file2 = tmp_path / "fail.pdf"
|
|
file1.write_text("content1")
|
|
file2.write_text("content2")
|
|
|
|
# First upload succeeds, second fails, third succeeds
|
|
response1 = {"id": "file-1", "filename": "success.pdf"}
|
|
response2 = {} # No ID - will fail
|
|
response3 = {"id": "file-3", "filename": "fail.pdf"}
|
|
|
|
with patch("openwebui_cli.commands.rag.create_client") as mock_client_factory:
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__.return_value = mock_client
|
|
mock_client.__exit__.return_value = None
|
|
|
|
# Mock three upload attempts
|
|
mock_client.post.side_effect = [
|
|
Mock(status_code=200, json=lambda: response1),
|
|
Mock(status_code=200, json=lambda: response2),
|
|
Mock(status_code=200, json=lambda: response3),
|
|
]
|
|
mock_client_factory.return_value = mock_client
|
|
|
|
result = runner.invoke(
|
|
app, ["rag", "files", "upload", str(file1), str(file2)]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Uploaded: success.pdf" in result.stdout
|
|
assert "Summary: 1 successful, 1 failed" in result.stdout
|