learningcircuit--local-deep-research
7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
507 行
17 KiB
Python
507 行
17 KiB
Python
"""
|
|
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())}")
|