"""Integration tests for RAG pipeline with main document.""" from pathlib import Path from unittest.mock import MagicMock, patch import pytest from src.config_loader import get_config from src.main_document_loader import reload_main_document_loader from src.rag_pipeline import RAGPipeline @pytest.fixture(autouse=True) def ensure_fail_silently(): """Ensure fail_silently is always set in config for all tests in this module.""" import copy config = get_config() # Reload singleton at the start to ensure clean state reload_main_document_loader() # Save original config with deep copy to avoid reference issues original_main_doc_config = copy.deepcopy(config._config.get("main_document", {})) # Ensure fail_silently is set if "main_document" not in config._config: config._config["main_document"] = {} if "fail_silently" not in config._config["main_document"]: config._config["main_document"]["fail_silently"] = True yield # Restore original config after test if original_main_doc_config: config._config["main_document"] = original_main_doc_config elif "main_document" in config._config: # If there was no original config, remove what we added del config._config["main_document"] # Also reload the singleton to ensure it picks up the restored config reload_main_document_loader() class TestRAGWithMainDocument: """Test RAG pipeline with main document integration.""" def test_rag_pipeline_initialization(self): """Test that RAG pipeline initializes with main document loader.""" pipeline = RAGPipeline() # Should have main_doc_loader attribute assert hasattr(pipeline, "main_doc_loader") assert hasattr(pipeline, "main_doc_content") def test_main_doc_info_disabled(self): """Test main document info when feature is disabled.""" # Temporarily disable main document config = get_config() # Save the entire main_document config section original_main_doc_config = config._config.get("main_document", {}).copy() try: # Mock disabled state but preserve other config values config._config["main_document"] = original_main_doc_config.copy() config._config["main_document"]["enabled"] = False pipeline = RAGPipeline() info = pipeline.get_main_document_info() assert "enabled" in info assert "loaded" in info assert info["enabled"] is False assert info["loaded"] is False finally: # Restore original config completely if original_main_doc_config: config._config["main_document"] = original_main_doc_config def test_main_doc_info_enabled(self): """Test main document info when feature is enabled and loaded.""" config = get_config() # Only run if main document is actually configured and exists main_doc_path = config.get("main_document.path", "") if not main_doc_path or not Path(main_doc_path).exists(): pytest.skip("Main document not configured or doesn't exist") pipeline = RAGPipeline() # If main document is loaded if pipeline.main_doc_content: info = pipeline.get_main_document_info() assert info["enabled"] is True assert info["loaded"] is True assert "tokens" in info assert "path" in info assert "size_bytes" in info assert info["tokens"] > 0 assert info["size_bytes"] > 0 def test_token_budget_calculation(self): """Test token budget calculation.""" pipeline = RAGPipeline() budget = pipeline._calculate_context_budget() # Check all required keys are present assert "model_context_window" in budget assert "main_doc_tokens" in budget assert "max_output_tokens" in budget assert "buffer_tokens" in budget assert "estimated_chat_history_tokens" in budget assert "available_for_retrieval" in budget assert "total_input_budget" in budget # Check reasonable values assert budget["model_context_window"] > 0 assert budget["main_doc_tokens"] >= 0 assert budget["max_output_tokens"] > 0 assert budget["buffer_tokens"] >= 0 assert budget["estimated_chat_history_tokens"] >= 0 assert budget["available_for_retrieval"] >= 0 # Check math (must account for estimated chat history tokens) expected_available = ( budget["model_context_window"] - budget["main_doc_tokens"] - budget["max_output_tokens"] - budget["buffer_tokens"] - budget["estimated_chat_history_tokens"] ) assert budget["available_for_retrieval"] == max(0, expected_available) def test_format_main_doc_empty(self): """Test _format_main_doc returns empty string when no content.""" pipeline = RAGPipeline() pipeline.main_doc_content = "" formatted = pipeline._format_main_doc() assert formatted == "" def test_format_main_doc_with_content(self): """Test _format_main_doc formats content correctly.""" pipeline = RAGPipeline() pipeline.main_doc_content = "Test profile content" formatted = pipeline._format_main_doc() assert "ESSENTIAL PROFILE INFORMATION" in formatted assert "Test profile content" in formatted assert "ADDITIONAL CONTEXT FROM DOCUMENTS" in formatted def test_reload_main_document(self, tmp_path): """Test runtime reloading of main document.""" # Create a test main document test_doc = tmp_path / "test_profile.md" test_doc.write_text("# Test Profile\n\nOriginal content") # Temporarily override config config = get_config() # Save the entire main_document config section original_main_doc_config = config._config.get("main_document", {}).copy() try: config._config["main_document"] = { "enabled": True, "path": str(test_doc), "max_tokens": 10000, "summarize_if_exceeds": True, "cache_enabled": True, "cache_check_interval": 60, "fail_silently": True, # Add missing key "fallback_to_vectordb_only": True, } # Reload the singleton to pick up new config reload_main_document_loader() pipeline = RAGPipeline() # Initial content assert "Original content" in pipeline.main_doc_content # Modify file test_doc.write_text("# Test Profile\n\nModified content") # Reload success = pipeline.reload_main_document() assert success is True assert "Modified content" in pipeline.main_doc_content finally: # Restore original config completely if original_main_doc_config: config._config["main_document"] = original_main_doc_config def test_reload_main_document_error_handling(self): """Test reload_main_document handles errors gracefully.""" pipeline = RAGPipeline() # Set invalid path pipeline.main_doc_loader.path = Path("/nonexistent/path/file.md") # Reload should succeed (with empty content) when fail_silently is True success = pipeline.reload_main_document() assert success is True # But content should be empty since file doesn't exist assert pipeline.main_doc_content == "" def test_query_with_main_doc(self): """Test that query works with main document enabled.""" config = get_config() # Only run if main document is configured main_doc_enabled = config.get("main_document.enabled", False) if not main_doc_enabled: pytest.skip("Main document not enabled in config") pipeline = RAGPipeline() # Simple test query response = pipeline.query("What is the name?") assert "result" in response assert isinstance(response["result"], str) assert len(response["result"]) > 0 def test_format_docs_empty_list(self): """Test _format_docs handles empty list.""" pipeline = RAGPipeline() formatted = pipeline._format_docs([]) assert formatted == "" def test_context_budget_warning_large_main_doc(self, caplog): """Test that warning is logged when main doc uses >50% of context.""" pipeline = RAGPipeline() # Simulate large main document original_content = pipeline.main_doc_content pipeline.main_doc_content = "Test " * 10000 # Very large content try: import logging with caplog.at_level(logging.WARNING): budget = pipeline._calculate_context_budget() # Check if warning was logged (only if main doc is actually large) if budget["main_doc_tokens"] > budget["model_context_window"] * 0.5: assert any("uses" in record.message for record in caplog.records) finally: # Restore original content pipeline.main_doc_content = original_content def test_model_context_window_detection(self): """Test that different models get correct context windows.""" config = get_config() original_model = config.get("llm.model", "") test_cases = { "llama3.2:3b": 8192, "llama3.1:8b": 128000, "phi3:mini": 4096, "gemma2:2b": 8192, "unknown:model": 8192, # Default } # Mock LLM handler to avoid actual LLM initialization with patch("src.rag_pipeline.get_llm_handler") as mock_llm_handler: mock_handler = MagicMock() mock_handler.get_llm.return_value = MagicMock() mock_llm_handler.return_value = mock_handler for model_name, expected_window in test_cases.items(): config._config["llm"]["model"] = model_name pipeline = RAGPipeline() budget = pipeline._calculate_context_budget() assert budget["model_context_window"] == expected_window # Restore original model config._config["llm"]["model"] = original_model class TestMainDocumentContextInsertion: """Test that main document content is properly inserted into prompts.""" def test_prompt_template_structure(self): """Test that prompt template includes main document placeholder.""" # Mock LLM handler to avoid actual LLM initialization with patch("src.rag_pipeline.get_llm_handler") as mock_llm_handler: mock_handler = MagicMock() mock_handler.get_llm.return_value = MagicMock() mock_llm_handler.return_value = mock_handler pipeline = RAGPipeline() template_str = pipeline.prompt_template.messages[0].prompt.template # Should contain main_document_section placeholder assert "{main_document_section}" in template_str assert "{context}" in template_str assert "{question}" in template_str def test_main_doc_position_before_context(self): """Test that main document appears BEFORE vectordb context in template.""" # Mock LLM handler to avoid actual LLM initialization with patch("src.rag_pipeline.get_llm_handler") as mock_llm_handler: mock_handler = MagicMock() mock_handler.get_llm.return_value = MagicMock() mock_llm_handler.return_value = mock_handler pipeline = RAGPipeline() template_str = pipeline.prompt_template.messages[0].prompt.template main_doc_pos = template_str.find("{main_document_section}") context_pos = template_str.find("{context}") # Main document should come before context assert main_doc_pos < context_pos