""" Test that all strategies can execute analyze_topic() with mocked dependencies. This is Level 3 testing - verifying the main method works without crashing. """ import pytest from loguru import logger from unittest.mock import Mock from .conftest import FACTORY_STRATEGY_NAMES # Strategies known to have complex requirements or be experimental SLOW_STRATEGIES = [ "evidence", "constrained", "parallel-constrained", "early-stop-constrained", "smart-query", "dual-confidence", "dual-confidence-with-rejection", "concurrent-dual-confidence", "constraint-parallel", "modular", "modular-parallel", "browsecomp", "browsecomp-entity", ] # Core strategies that should definitely work CORE_STRATEGIES = [ "source-based", "rapid", "parallel", "iterdrag", "standard", "news", "recursive", ] class TestCoreStrategiesAnalyzeTopic: """Test analyze_topic for core strategies that should definitely work.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_analyze_topic_returns_dict( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that analyze_topic returns a dict with expected keys.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) try: result = strategy.analyze_topic("What is artificial intelligence?") # Should return a dict assert isinstance(result, dict), ( f"{strategy_name} returned {type(result)} instead of dict" ) # Should have key attributes (may vary by strategy) # Most strategies should return at least these logger.info(f"{strategy_name} returned keys: {list(result.keys())}") # Verify commonly expected keys if "error" in result: logger.warning( f"{strategy_name} returned with error: {result.get('error')}" ) except Exception as e: pytest.fail( f"{strategy_name}.analyze_topic failed: {type(e).__name__}: {e}" ) @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_analyze_topic_with_progress_callback( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that progress callbacks are called during analyze_topic.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) callback = Mock() strategy.set_progress_callback(callback) try: strategy.analyze_topic("Test query") # Callback should have been called at least once if callback.call_count > 0: logger.info( f"{strategy_name} called progress callback {callback.call_count} times" ) else: logger.warning( f"{strategy_name} never called progress callback" ) except Exception as e: pytest.fail(f"{strategy_name} failed with callback: {e}") class TestAllStrategiesAnalyzeTopic: """Test analyze_topic for all factory strategies.""" @pytest.mark.parametrize("strategy_name", FACTORY_STRATEGY_NAMES) def test_analyze_topic_does_not_crash( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """ Test that analyze_topic doesn't crash for any strategy. This test documents which strategies work and which have issues, without failing the entire test suite. """ from local_deep_research.search_system_factory import ( create_strategy, ) try: strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) result = strategy.analyze_topic( "Test query for strategy validation" ) # Success - log what we got assert isinstance(result, dict), ( f"Expected dict, got {type(result)}" ) keys = list(result.keys()) logger.info( f"[OK] {strategy_name}: returned {len(keys)} keys: {keys[:5]}..." ) # Check for error in result if "error" in result and result["error"]: logger.warning( f"[WARN] {strategy_name} returned error: {result['error']}" ) except AttributeError as e: # Log attribute errors - often indicate missing mock methods logger.exception(f"[ATTR ERROR] {strategy_name}") pytest.skip(f"{strategy_name} has attribute issues: {e}") except TypeError as e: # Type errors often indicate constructor or method signature issues logger.exception(f"[TYPE ERROR] {strategy_name}") pytest.skip(f"{strategy_name} has type issues: {e}") except Exception as e: # Other exceptions - log for analysis logger.exception(f"[ERROR] {strategy_name}: {type(e).__name__}") pytest.skip(f"{strategy_name} failed: {type(e).__name__}: {e}") class TestAnalyzeTopicReturnStructure: """Test the structure of analyze_topic return values.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_result_has_findings_key( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that result contains 'findings' key.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) result = strategy.analyze_topic("Test query") # findings key should exist (may be empty list) assert "findings" in result or "error" in result, ( f"{strategy_name} missing 'findings' key. Keys: {list(result.keys())}" ) @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_result_has_current_knowledge( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that result contains current_knowledge.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) result = strategy.analyze_topic("Test query") # Should have current_knowledge (the synthesized answer) if "error" not in result: assert "current_knowledge" in result, ( f"{strategy_name} missing 'current_knowledge'. Keys: {list(result.keys())}" ) class TestLinksAccumulation: """Test that strategies properly accumulate links.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_links_populated_after_analyze( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that all_links_of_system is populated after analyze_topic.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) # Start empty assert len(strategy.all_links_of_system) == 0 strategy.analyze_topic("Test query") # After search, should have some links (from mock search results) logger.info( f"{strategy_name} accumulated {len(strategy.all_links_of_system)} links" ) class TestQuestionsTracking: """Test that strategies track generated questions.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_questions_by_iteration_populated( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test that questions_by_iteration is populated.""" from local_deep_research.search_system_factory import ( create_strategy, ) strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=strategy_mock_search, settings_snapshot=strategy_settings_snapshot, ) result = strategy.analyze_topic("Test query") # Should have questions tracked questions = strategy.questions_by_iteration logger.info( f"{strategy_name} tracked {len(questions)} iterations of questions" ) # Result may also contain questions if "questions_by_iteration" in result: logger.info( f"Result contains questions_by_iteration with {len(result['questions_by_iteration'])} iterations" ) class TestErrorHandling: """Test strategy behavior when search returns errors or empty results.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_handles_empty_search_results( self, strategy_name: str, strategy_mock_llm, strategy_settings_snapshot, ): """Test that strategy handles empty search results gracefully.""" from local_deep_research.search_system_factory import ( create_strategy, ) # Mock search that returns empty results empty_search = Mock() empty_search.run = Mock(return_value=[]) empty_search.include_full_content = True strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=empty_search, settings_snapshot=strategy_settings_snapshot, ) try: result = strategy.analyze_topic("Test query with no results") # Should not crash - may return error or empty findings assert isinstance(result, dict) logger.info( f"{strategy_name} handled empty results: {list(result.keys())}" ) except Exception as e: pytest.fail(f"{strategy_name} crashed on empty results: {e}") @pytest.mark.parametrize( "strategy_name", CORE_STRATEGIES[:3] ) # Test subset def test_handles_search_exception( self, strategy_name: str, strategy_mock_llm, strategy_settings_snapshot, ): """Test that strategy handles search exceptions gracefully.""" from local_deep_research.search_system_factory import ( create_strategy, ) # Mock search that raises an exception error_search = Mock() error_search.run = Mock(side_effect=Exception("Search API error")) error_search.include_full_content = True strategy = create_strategy( strategy_name=strategy_name, model=strategy_mock_llm, search=error_search, settings_snapshot=strategy_settings_snapshot, ) try: result = strategy.analyze_topic("Test query") # Should handle the error gracefully assert isinstance(result, dict) # Should indicate an error occurred if "error" in result: logger.info( f"{strategy_name} returned error: {result['error']}" ) except Exception as e: # Some strategies may propagate the error logger.warning(f"{strategy_name} propagated error: {e}") def test_source_based_scrubs_exception_text_from_error_fields( self, strategy_mock_llm, strategy_settings_snapshot, ): """CWE-209 / CodeQL #8019 regression guard: the except block in SourceBasedSearchStrategy must run exception text through sanitize_error_for_client before it lands in the client-visible current_knowledge / formatted_findings / finding-content fields. Unlike test_handles_search_exception above, a failure here fails the test — no try/except softening — so a regression to raw ``f"Error: {e!s}"`` (or a crash inside the except block) is caught. A search-level exception never reaches that except block (the per-question search path swallows it into a "No sources were found" summary), so the failure is injected at ``findings_repository.format_findings_to_text`` — called inside the guarded block right before the return. """ from local_deep_research.search_system_factory import ( create_strategy, ) secret = "sk-STRATEGYLEAK123456789012" mock_search = Mock() mock_search.run = Mock( return_value=[ { "title": "Result", "link": "https://example.com/1", "snippet": "A result snippet", } ] ) mock_search.include_full_content = True strategy = create_strategy( strategy_name="source-based", model=strategy_mock_llm, search=mock_search, settings_snapshot=strategy_settings_snapshot, ) # The categorizable token sits past char 200 of the scrubbed # message: it must survive the strategy-layer cap (500, aligned # with _ERROR_BOUNDARY_MAX_LEN / _TOOL_ERROR_MAX_LEN). With # sanitize_error_for_client's 200-char default it would be # truncated away before the API boundary ever sees it. padding = "x" * 230 strategy.findings_repository.format_findings_to_text = Mock( side_effect=Exception( f"401 Unauthorized: https://api.example.com/v1?api_key={secret}" f" {padding} Connection refused [Errno 111]" ) ) result = strategy.analyze_topic("Test query") assert result["current_knowledge"].startswith("Error:") assert result["formatted_findings"].startswith("Error:") assert secret not in result["current_knowledge"] assert secret not in result["formatted_findings"] assert "Connection refused" in result["current_knowledge"] error_contents = [ f["content"] for f in result["findings"] if isinstance(f, dict) and isinstance(f.get("content"), str) ] assert error_contents, "expected at least one finding with content" assert all(secret not in content for content in error_contents) class TestSearchSystemIntegration: """Test analyze_topic through AdvancedSearchSystem.""" @pytest.mark.parametrize("strategy_name", CORE_STRATEGIES) def test_search_system_analyze_topic( self, strategy_name: str, strategy_mock_llm, strategy_mock_search, strategy_settings_snapshot, ): """Test analyze_topic through the full AdvancedSearchSystem.""" from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=strategy_mock_llm, search=strategy_mock_search, strategy_name=strategy_name, settings_snapshot=strategy_settings_snapshot, ) result = system.analyze_topic("Test query") assert isinstance(result, dict) assert "search_system" in result # AdvancedSearchSystem adds this assert result["search_system"] == system logger.info(f"SearchSystem with {strategy_name}: {list(result.keys())}")