acb / tests /test_app.py
Kagan Tek
merge
79d4fd5
Raw
History Blame Contribute Delete
21.1 kB
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import pytest
import hmac
from unittest.mock import Mock, MagicMock, patch
class TestCheckPassword:
"""Tests for check_password function."""
@patch("app.st")
def test_returns_true_when_already_authenticated(self, mock_st):
"""Should return True if password_correct is True."""
from app import check_password
mock_st.session_state = MagicMock()
mock_st.session_state.__contains__ = Mock(return_value=True)
mock_st.session_state.password_correct = True
result = check_password()
assert result is True
@patch("app.APP_PASSWORD", "")
@patch("app.st")
def test_returns_true_when_no_password_configured(self, mock_st):
"""Should return True when APP_PASSWORD is empty."""
from app import check_password
mock_st.session_state = MagicMock()
mock_st.session_state.__contains__ = Mock(return_value=False)
result = check_password()
assert result is True
@patch("app.APP_PASSWORD", "correct")
@patch("app.st")
def test_returns_false_when_wrong_password(self, mock_st):
"""Should return False for wrong password."""
from app import check_password
mock_st.session_state = MagicMock()
mock_st.session_state.__contains__ = Mock(return_value=False)
mock_st.text_input.return_value = "wrong"
result = check_password()
assert result is False
mock_st.error.assert_called_once()
@patch("app.APP_PASSWORD", "correct")
@patch("app.st")
def test_returns_false_when_no_password_entered(self, mock_st):
"""Should return False when no password entered."""
from app import check_password
mock_st.session_state = MagicMock()
mock_st.session_state.__contains__ = Mock(return_value=False)
mock_st.text_input.return_value = ""
result = check_password()
assert result is False
class TestProcessUploadedFiles:
"""Tests for process_uploaded_files function."""
@patch("app.get_vector_store")
@patch("app.extract_pdf_pages")
@patch("app.process_documents")
@patch("app.chunks_to_store_format")
def test_processes_single_file(self, mock_format, mock_process, mock_extract, mock_get_store):
"""Should process a single PDF file."""
from app import process_uploaded_files
mock_file = Mock()
mock_file.name = "test.pdf"
mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}]
mock_process.return_value = [{"text": "chunk", "metadata": {}}]
mock_format.return_value = (["chunk"], [{}])
mock_store = Mock()
mock_store.get_loaded_sources.return_value = set()
mock_get_store.return_value = mock_store
result = process_uploaded_files([mock_file])
assert result == 1
mock_store.add_documents.assert_called_once()
@patch("app.get_vector_store")
@patch("app.extract_pdf_pages")
@patch("app.process_documents")
@patch("app.chunks_to_store_format")
def test_processes_multiple_files(self, mock_format, mock_process, mock_extract, mock_get_store):
"""Should process multiple PDF files."""
from app import process_uploaded_files
mock_file1 = Mock()
mock_file1.name = "test1.pdf"
mock_file2 = Mock()
mock_file2.name = "test2.pdf"
mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}]
mock_process.return_value = [{"text": "chunk", "metadata": {}}]
mock_format.return_value = (["chunk"], [{}])
mock_store = Mock()
mock_store.get_loaded_sources.return_value = set()
mock_get_store.return_value = mock_store
result = process_uploaded_files([mock_file1, mock_file2])
assert result == 2
assert mock_store.add_documents.call_count == 2
def test_empty_files_returns_zero(self):
"""Should return 0 for empty file list."""
from app import process_uploaded_files
result = process_uploaded_files([])
assert result == 0
def test_none_files_returns_zero(self):
"""Should return 0 for None."""
from app import process_uploaded_files
result = process_uploaded_files(None)
assert result == 0
@patch("app.get_vector_store")
@patch("app.extract_pdf_pages")
@patch("app.process_documents")
def test_skips_empty_chunks(self, mock_process, mock_extract, mock_get_store):
"""Should skip files with no chunks."""
from app import process_uploaded_files
mock_file = Mock()
mock_file.name = "empty.pdf"
mock_extract.return_value = []
mock_process.return_value = []
mock_store = Mock()
mock_store.get_loaded_sources.return_value = set()
mock_get_store.return_value = mock_store
result = process_uploaded_files([mock_file])
assert result == 0
mock_store.add_documents.assert_not_called()
class TestResetApplication:
"""Tests for reset_application function."""
@patch("app.reset_llm_generator")
@patch("app.reset_retrieval_engine")
@patch("app.reset_vector_store")
@patch("app.reset_embedder")
@patch("app.st")
def test_resets_all_components(self, mock_st, mock_reset_embedder, mock_reset_store, mock_reset_retrieval, mock_reset_llm):
"""Should reset vector store and embedder."""
from app import reset_application
mock_st.session_state = MagicMock()
reset_application()
mock_reset_store.assert_called_once()
mock_reset_embedder.assert_called_once()
mock_reset_retrieval.assert_called_once()
mock_reset_llm.assert_called_once()
@patch("app.reset_llm_generator")
@patch("app.reset_retrieval_engine")
@patch("app.reset_vector_store")
@patch("app.reset_embedder")
@patch("app.st")
def test_clears_session_state(self, mock_st, mock_reset_embedder, mock_reset_store, mock_reset_retrieval, mock_reset_llm):
"""Should clear session state values."""
from app import reset_application
mock_st.session_state = MagicMock()
mock_st.session_state.documents_processed = True
mock_st.session_state.chunk_count = 100
mock_st.session_state.chat_history = [{"role": "user", "content": "hi"}]
mock_st.session_state.sources = [{}]
reset_application()
assert mock_st.session_state.documents_processed is False
assert mock_st.session_state.chunk_count == 0
assert mock_st.session_state.chat_history == []
assert mock_st.session_state.sources == []
class TestInitSessionState:
"""Tests for init_session_state function."""
@patch("app.st")
def test_initializes_missing_keys(self, mock_st):
"""Should initialize missing session state keys."""
from app import init_session_state
mock_st.session_state = {}
init_session_state()
assert "documents_processed" in mock_st.session_state
assert "chunk_count" in mock_st.session_state
assert "chat_history" in mock_st.session_state
assert "selected_model" in mock_st.session_state
assert "use_reranking" in mock_st.session_state
@patch("app.st")
def test_preserves_existing_values(self, mock_st):
"""Should not overwrite existing session state values."""
from app import init_session_state
mock_st.session_state = {"chunk_count": 50}
init_session_state()
assert mock_st.session_state["chunk_count"] == 50
@patch("app.st")
def test_default_values(self, mock_st):
"""Should set correct default values."""
from app import init_session_state
from config import DEFAULT_LLM_MODEL
mock_st.session_state = {}
init_session_state()
assert mock_st.session_state["documents_processed"] is False
assert mock_st.session_state["chunk_count"] == 0
assert mock_st.session_state["chat_history"] == []
assert mock_st.session_state["use_reranking"] is True
assert mock_st.session_state["selected_model"] == DEFAULT_LLM_MODEL
class TestGenerateResponse:
"""Tests for generate_response function."""
@pytest.fixture(autouse=True)
def reset_handler(self):
from src.integration.query_handler import reset_query_handler
reset_query_handler()
yield
reset_query_handler()
@patch("app.get_query_handler")
def test_returns_response_sources_debug(self, mock_get_handler):
from app import generate_response
mock_handler = MagicMock()
mock_result = MagicMock()
mock_result.response = "response text"
mock_result.sources = [{"source": "test.pdf"}]
mock_result.mode.value = "documents"
mock_result.execution_time = 0.5
mock_result.intent = None
mock_result.sql_query = None
mock_result.db_results = None
mock_result.success = True
mock_result.error = None
mock_result.routing_metadata = None
mock_handler.execute.return_value = mock_result
mock_get_handler.return_value = mock_handler
response, sources, debug = generate_response("question", True, [], "model-id")
assert response == "response text"
assert sources == [{"source": "test.pdf"}]
assert debug["success"] is True
@patch("app.get_query_handler")
def test_passes_reranking_flag(self, mock_get_handler):
from app import generate_response
mock_handler = MagicMock()
mock_result = MagicMock()
mock_result.response = ""
mock_result.sources = []
mock_result.mode.value = "documents"
mock_result.execution_time = 0.1
mock_result.intent = None
mock_result.sql_query = None
mock_result.db_results = None
mock_result.success = True
mock_result.error = None
mock_result.routing_metadata = None
mock_handler.execute.return_value = mock_result
mock_get_handler.return_value = mock_handler
generate_response("q", False, [], "model")
mock_handler.execute.assert_called_once()
@patch("app.get_query_handler")
def test_passes_chat_history_to_retrieve(self, mock_get_handler):
from app import generate_response
mock_handler = MagicMock()
mock_result = MagicMock()
mock_result.response = ""
mock_result.sources = []
mock_result.mode.value = "documents"
mock_result.execution_time = 0.1
mock_result.intent = None
mock_result.sql_query = None
mock_result.db_results = None
mock_result.success = True
mock_result.error = None
mock_result.routing_metadata = None
mock_handler.execute.return_value = mock_result
mock_get_handler.return_value = mock_handler
history = [{"role": "user", "content": "Atlas nedir?"}]
generate_response("Bunun ozellikleri?", True, history, "model")
call_kwargs = mock_handler.execute.call_args
assert call_kwargs[1]["chat_history"] == history
@patch("app.get_query_handler")
def test_limits_chat_history(self, mock_get_handler):
from app import generate_response
mock_handler = MagicMock()
mock_result = MagicMock()
mock_result.response = ""
mock_result.sources = []
mock_result.mode.value = "documents"
mock_result.execution_time = 0.1
mock_result.intent = None
mock_result.sql_query = None
mock_result.db_results = None
mock_result.success = True
mock_result.error = None
mock_result.routing_metadata = None
mock_handler.execute.return_value = mock_result
mock_get_handler.return_value = mock_handler
long_history = [{"role": "user", "content": f"msg{i}"} for i in range(10)]
generate_response("q", True, long_history, "model")
call_kwargs = mock_handler.execute.call_args
assert call_kwargs[1]["chat_history"] == long_history
@patch("app.get_query_handler")
def test_passes_model_id(self, mock_get_handler):
from app import generate_response
mock_handler = MagicMock()
mock_result = MagicMock()
mock_result.response = ""
mock_result.sources = []
mock_result.mode.value = "documents"
mock_result.execution_time = 0.1
mock_result.intent = None
mock_result.sql_query = None
mock_result.db_results = None
mock_result.success = True
mock_result.error = None
mock_result.routing_metadata = None
mock_handler.execute.return_value = mock_result
mock_get_handler.return_value = mock_handler
generate_response("q", True, [], "custom-model")
call_kwargs = mock_handler.execute.call_args
assert call_kwargs[1]["model_id"] == "custom-model"
class TestLoadCss:
"""Tests for load_css function."""
@patch("app.STYLES_PATH")
def test_loads_css_from_file(self, mock_path):
"""Should load CSS content from file."""
from app import load_css
mock_path.exists.return_value = True
mock_path.read_text.return_value = ".test { color: red; }"
result = load_css()
assert result == ".test { color: red; }"
@patch("app.STYLES_PATH")
def test_returns_empty_if_file_missing(self, mock_path):
"""Should return empty string if file doesn't exist."""
from app import load_css
mock_path.exists.return_value = False
result = load_css()
assert result == ""
class TestApplyCustomCss:
"""Tests for apply_custom_css function."""
@patch("app.load_css")
@patch("app.st")
def test_calls_markdown_with_css(self, mock_st, mock_load):
"""Should inject CSS via st.markdown."""
from app import apply_custom_css
mock_load.return_value = ".test { color: red; }"
apply_custom_css()
mock_st.markdown.assert_called_once()
call_args = mock_st.markdown.call_args
assert "unsafe_allow_html" in call_args[1]
assert call_args[1]["unsafe_allow_html"] is True
@patch("app.load_css")
@patch("app.st")
def test_skips_if_no_css(self, mock_st, mock_load):
"""Should not call markdown if CSS is empty."""
from app import apply_custom_css
mock_load.return_value = ""
apply_custom_css()
mock_st.markdown.assert_not_called()
@patch("app.load_css")
@patch("app.st")
def test_wraps_css_in_style_tags(self, mock_st, mock_load):
"""Should wrap CSS content in style tags."""
from app import apply_custom_css
mock_load.return_value = ".box { padding: 10px; }"
apply_custom_css()
call_args = mock_st.markdown.call_args[0][0]
assert "<style>" in call_args
assert "</style>" in call_args
assert ".box { padding: 10px; }" in call_args
class TestGetBaseDocuments:
"""Tests for get_base_documents function."""
@patch("app.BASE_DOCUMENTS_PATH")
def test_returns_empty_if_path_not_exists(self, mock_path):
"""Should return empty list if documents folder doesn't exist."""
from app import get_base_documents
mock_path.exists.return_value = False
result = get_base_documents()
assert result == []
@patch("app.BASE_DOCUMENTS_PATH")
def test_returns_pdf_files(self, mock_path):
"""Should return list of PDF files."""
from app import get_base_documents
mock_path.exists.return_value = True
mock_path.glob.return_value = [Path("/docs/a.pdf"), Path("/docs/b.pdf")]
result = get_base_documents()
assert len(result) == 2
mock_path.glob.assert_called_with("*.pdf")
class TestLoadDefaultDocuments:
"""Tests for load_default_documents function."""
@patch("app.get_base_documents")
@patch("app.st")
def test_skips_if_already_loaded(self, mock_st, mock_get_docs):
"""Should skip if base_docs_loaded is True."""
from app import load_default_documents
mock_st.session_state.get.return_value = True
load_default_documents()
mock_get_docs.assert_not_called()
@patch("app.get_vector_store")
@patch("app.get_base_documents")
@patch("app.st")
def test_skips_if_no_documents(self, mock_st, mock_get_docs, mock_get_store):
"""Should set flag and return if no documents."""
from app import load_default_documents
mock_st.session_state = MagicMock()
mock_st.session_state.get.return_value = False
mock_get_docs.return_value = []
load_default_documents()
assert mock_st.session_state.base_docs_loaded is True
@patch("app.chunks_to_store_format")
@patch("app.process_documents")
@patch("app.extract_pdf_from_path")
@patch("app.get_vector_store")
@patch("app.get_base_documents")
@patch("app.st")
def test_processes_documents(
self, mock_st, mock_get_docs, mock_get_store,
mock_extract, mock_process, mock_format
):
"""Should process and store base documents."""
from app import load_default_documents
mock_st.session_state = MagicMock()
mock_st.session_state.get.return_value = False
mock_get_docs.return_value = [Path("/docs/test.pdf")]
mock_store = MagicMock()
mock_store.get_collection_stats.return_value = {"points_count": 0}
mock_get_store.return_value = mock_store
mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}]
mock_process.return_value = [{"text": "chunk", "metadata": {}}]
mock_format.return_value = (["chunk"], [{}])
load_default_documents()
mock_store.add_documents.assert_called_once()
assert mock_st.session_state.base_docs_loaded is True
assert mock_st.session_state.documents_processed is True
class TestRenderDebugInfo:
"""Tests for render_debug_info function."""
@patch("app.st")
def test_shows_query_rewriting_when_rewritten(self, mock_st):
"""Should display query rewriting section when query was rewritten."""
from app import render_debug_info
mock_col = MagicMock()
mock_col.__enter__ = MagicMock(return_value=mock_col)
mock_col.__exit__ = MagicMock(return_value=False)
mock_st.columns.side_effect = [
[mock_col, mock_col],
[mock_col, mock_col, mock_col]
]
debug_info = {
"query": "Bunun ozellikleri?",
"search_query": "Atlas ERP ozellikleri nelerdir?",
"query_rewritten": True,
"rewrite_info": {
"is_follow_up": True,
"method": "llm",
"rewrite_time_ms": 150
}
}
render_debug_info(debug_info)
mock_st.subheader.assert_called()
assert mock_st.columns.call_count == 2
@patch("app.st")
def test_skips_rewriting_section_when_not_rewritten(self, mock_st):
"""Should skip query rewriting section when query was not rewritten."""
from app import render_debug_info
debug_info = {
"query": "Atlas nedir?",
"search_query": "Atlas nedir?",
"query_rewritten": False,
"rewrite_info": None
}
render_debug_info(debug_info)
mock_st.info.assert_not_called()
mock_st.success.assert_not_called()
@patch("app.st")
def test_always_shows_retrieval_details(self, mock_st):
"""Should always show retrieval details JSON."""
from app import render_debug_info
debug_info = {
"query": "Test",
"query_rewritten": False,
"stage1_count": 10
}
render_debug_info(debug_info)
mock_st.json.assert_called_once()