"""Tests for report_generator module.""" from unittest.mock import MagicMock from local_deep_research.error_handling.report_generator import ( ErrorReportGenerator, ) from local_deep_research.error_handling.error_reporter import ErrorCategory class TestErrorReportGeneratorInit: """Tests for ErrorReportGenerator initialization.""" def test_initializes_error_reporter(self, error_report_generator): """Should initialize with ErrorReporter.""" assert hasattr(error_report_generator, "error_reporter") assert error_report_generator.error_reporter is not None def test_accepts_optional_llm(self): """Should accept optional LLM (kept for compatibility).""" mock_llm = MagicMock() generator = ErrorReportGenerator(llm=mock_llm) # LLM is not used, just accepted assert generator is not None class TestGenerateErrorReport: """Tests for generate_error_report method.""" def test_returns_markdown_string(self, error_report_generator): """Should return markdown string.""" result = error_report_generator.generate_error_report( "Connection refused", "test query" ) assert isinstance(result, str) assert "#" in result # Has markdown headers def test_includes_error_type(self, error_report_generator): """Should include error type in report.""" result = error_report_generator.generate_error_report( "Connection refused", "test query" ) assert "Error Type:" in result def test_includes_what_happened(self, error_report_generator): """Should include what happened section.""" result = error_report_generator.generate_error_report( "Connection refused", "test query" ) assert "What happened:" in result def test_includes_help_links(self, error_report_generator): """Should include help links.""" result = error_report_generator.generate_error_report( "Connection refused", "test query" ) assert "Get Help" in result assert "Wiki" in result assert "Discord" in result assert "GitHub Issues" in result def test_includes_partial_results_when_available( self, error_report_generator, sample_partial_results ): """Should include partial results when available.""" result = error_report_generator.generate_error_report( "Connection refused", "test query", partial_results=sample_partial_results, ) assert "Partial Results" in result def test_handles_no_partial_results(self, error_report_generator): """Should handle no partial results gracefully.""" result = error_report_generator.generate_error_report( "Connection refused", "test query", partial_results=None ) assert isinstance(result, str) # Should not include partial results section assert "Partial Results" not in result def test_includes_research_id_when_provided(self, error_report_generator): """Should accept research_id parameter.""" result = error_report_generator.generate_error_report( "Error", "query", research_id="test-uuid" ) assert isinstance(result, str) def test_includes_search_iterations_when_provided( self, error_report_generator ): """Should accept search_iterations parameter.""" result = error_report_generator.generate_error_report( "Error", "query", search_iterations=5 ) assert isinstance(result, str) def test_fallback_on_exception(self, error_report_generator): """Should return fallback report on exception.""" # Mock error_reporter to raise exception error_report_generator.error_reporter.analyze_error = MagicMock( side_effect=Exception("test error") ) result = error_report_generator.generate_error_report("Error", "query") assert "Research Failed" in result assert "Error report generation failed" in result class TestFormatPartialResults: """Tests for _format_partial_results method.""" def test_returns_empty_for_none(self, error_report_generator): """Should return empty string for None.""" result = error_report_generator._format_partial_results(None) assert result == "" def test_returns_empty_for_empty_dict(self, error_report_generator): """Should return empty string for empty dict.""" result = error_report_generator._format_partial_results({}) assert result == "" def test_formats_current_knowledge(self, error_report_generator): """Should format current_knowledge section.""" partial = {"current_knowledge": "This is knowledge content " * 10} result = error_report_generator._format_partial_results(partial) assert "Research Summary" in result def test_truncates_long_knowledge(self, error_report_generator): """Should truncate knowledge over 1000 chars.""" long_content = "x" * 1500 partial = {"current_knowledge": long_content} result = error_report_generator._format_partial_results(partial) assert "..." in result def test_skips_short_knowledge(self, error_report_generator): """Should skip knowledge under 50 chars.""" partial = {"current_knowledge": "short"} result = error_report_generator._format_partial_results(partial) assert "Research Summary" not in result def test_formats_search_results(self, error_report_generator): """Should format search_results section.""" partial = { "search_results": [ {"title": "Result 1", "url": "https://example.com/1"}, {"title": "Result 2", "url": "https://example.com/2"}, ] } result = error_report_generator._format_partial_results(partial) assert "Search Results Found" in result assert "Result 1" in result assert "https://example.com/1" in result def test_limits_search_results_to_five(self, error_report_generator): """Should show only top 5 search results.""" partial = { "search_results": [ {"title": f"Result {i}", "url": f"https://example.com/{i}"} for i in range(10) ] } result = error_report_generator._format_partial_results(partial) # Results are 0-indexed, so 5 results means indices 0-4 assert "Result 4" in result assert "Result 5" not in result def test_formats_findings(self, error_report_generator): """Should format findings section.""" partial = { "findings": [ {"phase": "Phase 1", "content": "Finding content 1"}, {"phase": "Phase 2", "content": "Finding content 2"}, ] } result = error_report_generator._format_partial_results(partial) assert "Research Findings" in result assert "Phase 1" in result def test_skips_error_findings(self, error_report_generator): """Should skip findings that start with Error:.""" partial = { "findings": [ {"phase": "Phase 1", "content": "Error: Something went wrong"}, {"phase": "Phase 2", "content": "Valid finding"}, ] } result = error_report_generator._format_partial_results(partial) assert "Valid finding" in result # Error finding should be skipped def test_limits_findings_to_three(self, error_report_generator): """Should show only top 3 findings.""" partial = { "findings": [ {"phase": f"Phase {i}", "content": f"Finding {i}"} for i in range(5) ] } result = error_report_generator._format_partial_results(partial) assert "Phase 2" in result assert "Phase 4" not in result def test_truncates_long_finding_content(self, error_report_generator): """Should truncate finding content over 500 chars.""" partial = {"findings": [{"phase": "Phase 1", "content": "x" * 600}]} result = error_report_generator._format_partial_results(partial) assert "..." in result def test_includes_note_when_results_exist(self, error_report_generator): """Should include note about successful collection.""" partial = { "search_results": [ {"title": "Result", "url": "https://example.com"} ] } result = error_report_generator._format_partial_results(partial) assert "successfully collected" in result class TestGenerateQuickErrorSummary: """Tests for generate_quick_error_summary method.""" def test_returns_dict(self, error_report_generator): """Should return dictionary.""" result = error_report_generator.generate_quick_error_summary( "Connection refused" ) assert isinstance(result, dict) def test_has_expected_keys(self, error_report_generator): """Should have expected keys.""" result = error_report_generator.generate_quick_error_summary( "Connection refused" ) expected_keys = ["title", "category", "severity", "recoverable"] for key in expected_keys: assert key in result def test_category_is_string_value(self, error_report_generator): """Should return category as string value.""" result = error_report_generator.generate_quick_error_summary( "Connection refused" ) assert isinstance(result["category"], str) def test_recoverable_is_boolean(self, error_report_generator): """Should return recoverable as boolean.""" result = error_report_generator.generate_quick_error_summary( "Connection refused" ) assert isinstance(result["recoverable"], bool) class TestMakeErrorUserFriendly: """Tests for _make_error_user_friendly method.""" def test_replaces_max_workers_error(self, error_report_generator): """Should replace max_workers error with friendly message.""" result = error_report_generator._make_error_user_friendly( "max_workers must be greater than 0" ) assert "LLM failed to generate" in result assert "Try this:" in result def test_replaces_ollama_eof_error(self, error_report_generator): """Should replace Ollama EOF error with friendly message.""" result = error_report_generator._make_error_user_friendly( "POST predict encountered EOF" ) assert "Lost connection to Ollama" in result def test_replaces_connection_refused(self, error_report_generator): """Should replace connection refused with friendly message.""" result = error_report_generator._make_error_user_friendly( "Connection refused" ) assert "Cannot connect to the LLM service" in result def test_replaces_search_too_long(self, error_report_generator): """Should replace search too long error.""" result = error_report_generator._make_error_user_friendly( "The search is longer than 256 characters" ) assert "search query is too long" in result def test_replaces_model_not_found(self, error_report_generator): """Should replace model not found error.""" result = error_report_generator._make_error_user_friendly( "Model gemma not found in Ollama" ) assert "model isn't available in Ollama" in result def test_replaces_api_key_error(self, error_report_generator): """Should replace API key errors.""" result = error_report_generator._make_error_user_friendly( "No auth credentials found" ) assert "API key is missing" in result def test_replaces_readonly_database(self, error_report_generator): """Should replace readonly database error.""" result = error_report_generator._make_error_user_friendly( "Attempt to write readonly database" ) assert "Permission issue" in result def test_replaces_docker_networking(self, error_report_generator): """Should replace Docker networking errors.""" # Pattern requires "host.*localhost.*Docker" or similar result = error_report_generator._make_error_user_friendly( "Cannot connect to host at localhost via Docker" ) # Should match docker pattern and suggest using host.docker.internal assert "Docker" in result or "host.docker.internal" in result def test_returns_original_for_unknown(self, error_report_generator): """Should return original message for unknown errors.""" original = "Some random error that has no pattern" result = error_report_generator._make_error_user_friendly(original) assert result == original def test_includes_technical_error_in_replacement( self, error_report_generator ): """Should include technical error in replaced message.""" result = error_report_generator._make_error_user_friendly( "max_workers must be greater than 0" ) assert "Technical error:" in result assert "max_workers" in result def test_case_insensitive_matching(self, error_report_generator): """Should match patterns case-insensitively.""" result = error_report_generator._make_error_user_friendly( "CONNECTION REFUSED" ) assert "Cannot connect" in result class TestGetTechnicalContext: """Tests for _get_technical_context method.""" def test_returns_empty_for_no_context(self, error_report_generator): """Should return empty string for no context.""" error_analysis = {"category": ErrorCategory.CONNECTION_ERROR} result = error_report_generator._get_technical_context( error_analysis, None ) # May still have category-based context assert isinstance(result, str) def test_includes_timing_info(self, error_report_generator): """Should include timing info when available.""" error_analysis = {"category": ErrorCategory.CONNECTION_ERROR} partial = { "start_time": "2024-01-01T10:00:00", "last_activity": "2024-01-01T10:05:00", } result = error_report_generator._get_technical_context( error_analysis, partial ) assert "Start Time:" in result def test_includes_model_info(self, error_report_generator): """Should include model info when available.""" error_analysis = {"category": ErrorCategory.MODEL_ERROR} partial = { "model_config": {"model_name": "gpt-4", "provider": "openai"} } result = error_report_generator._get_technical_context( error_analysis, partial ) assert "Model:" in result def test_includes_search_info(self, error_report_generator): """Should include search info when available.""" error_analysis = {"category": ErrorCategory.SEARCH_ERROR} partial = {"search_config": {"engine": "duckduckgo", "max_results": 10}} result = error_report_generator._get_technical_context( error_analysis, partial ) assert "Search Engine:" in result def test_includes_error_codes(self, error_report_generator): """Should include error codes when available.""" error_analysis = {"category": ErrorCategory.CONNECTION_ERROR} partial = {"status_code": 503, "error_code": "SERVICE_UNAVAILABLE"} result = error_report_generator._get_technical_context( error_analysis, partial ) assert "Status Code:" in result assert "Error Code:" in result def test_adds_category_specific_context(self, error_report_generator): """Should add category-specific context.""" error_analysis = {"category": ErrorCategory.CONNECTION_ERROR} result = error_report_generator._get_technical_context( error_analysis, {} ) assert "Network Error:" in result error_analysis = {"category": ErrorCategory.MODEL_ERROR} result = error_report_generator._get_technical_context( error_analysis, {} ) assert "Model Error:" in result