Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

421 lines
15 KiB
Python

"""
Tests for search_system.py - AdvancedSearchSystem class.
Tests cover:
- __init__ settings extraction, defaults, factory wiring, follow-up delegation, logging
- set_progress_callback forwarding to strategy
- analyze_topic search_id generation and delegation
- _perform_search settings extraction, progress callbacks, result assembly
- Duplicate link avoidance (id check)
- NewsSearchCallback error resilience
"""
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Helpers / fixtures
# ---------------------------------------------------------------------------
def _make_strategy_mock():
"""Return a MagicMock that behaves like a strategy returned by create_strategy."""
strategy = MagicMock()
strategy.analyze_topic.return_value = {"current_knowledge": "test"}
strategy.questions_by_iteration = []
strategy.all_links_of_system = []
strategy.set_progress_callback = MagicMock()
return strategy
# Patch paths: top-level imports can be patched on search_system directly,
# but create_strategy and NewsSearchCallback are lazily imported inside method
# bodies, so they must be patched at their source modules.
PATCH_CITATION = "local_deep_research.search_system.CitationHandler"
PATCH_QUESTION_GEN = (
"local_deep_research.search_system.StandardQuestionGenerator"
)
PATCH_FINDINGS = "local_deep_research.search_system.FindingsRepository"
PATCH_CREATE_STRATEGY = (
"local_deep_research.search_system_factory.create_strategy"
)
PATCH_ENHANCED_FOLLOWUP = (
"local_deep_research.search_system.EnhancedContextualFollowUpStrategy"
)
PATCH_NEWS_CALLBACK = (
"local_deep_research.news.core.search_integration.NewsSearchCallback"
)
@pytest.fixture()
def mock_deps():
"""Patch CitationHandler, StandardQuestionGenerator, FindingsRepository,
and create_strategy so that AdvancedSearchSystem can be instantiated
without real dependencies. Yields a dict of mock objects."""
with (
patch(PATCH_CITATION) as m_citation,
patch(PATCH_QUESTION_GEN) as m_qgen,
patch(PATCH_FINDINGS) as m_findings,
patch(PATCH_CREATE_STRATEGY) as m_create,
):
strategy = _make_strategy_mock()
m_create.return_value = strategy
yield {
"citation": m_citation,
"question_gen": m_qgen,
"findings": m_findings,
"create_strategy": m_create,
"strategy": strategy,
}
@pytest.fixture()
def mock_model():
return MagicMock(name="BaseChatModel")
@pytest.fixture()
def mock_search():
return MagicMock(name="BaseSearchEngine")
def _build_system(mock_model, mock_search, mock_deps, **kwargs):
"""Convenience: build an AdvancedSearchSystem with the standard mocks."""
from local_deep_research.search_system import AdvancedSearchSystem
return AdvancedSearchSystem(llm=mock_model, search=mock_search, **kwargs)
# ===================================================================
# __init__ tests
# ===================================================================
class TestInit:
"""Tests for AdvancedSearchSystem.__init__."""
def test_settings_snapshot_defaults_to_empty_dict(
self, mock_model, mock_search, mock_deps
):
system = _build_system(
mock_model, mock_search, mock_deps, settings_snapshot=None
)
assert system.settings_snapshot == {}
# -- max_iterations -------------------------------------------------
def test_max_iterations_default_is_one(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
assert system.max_iterations == 1
def test_max_iterations_from_settings_plain_value(
self, mock_model, mock_search, mock_deps
):
system = _build_system(
mock_model,
mock_search,
mock_deps,
settings_snapshot={"search.iterations": 5},
)
assert system.max_iterations == 5
def test_max_iterations_extracts_value_from_dict(
self, mock_model, mock_search, mock_deps
):
system = _build_system(
mock_model,
mock_search,
mock_deps,
settings_snapshot={"search.iterations": {"value": 8}},
)
assert system.max_iterations == 8
def test_max_iterations_explicit_overrides_snapshot(
self, mock_model, mock_search, mock_deps
):
system = _build_system(
mock_model,
mock_search,
mock_deps,
max_iterations=12,
settings_snapshot={"search.iterations": {"value": 8}},
)
assert system.max_iterations == 12
# -- questions_per_iteration ----------------------------------------
def test_questions_per_iteration_default_is_three(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
assert system.questions_per_iteration == 3
def test_questions_per_iteration_from_settings_dict(
self, mock_model, mock_search, mock_deps
):
system = _build_system(
mock_model,
mock_search,
mock_deps,
settings_snapshot={"search.questions_per_iteration": {"value": 7}},
)
assert system.questions_per_iteration == 7
# -- strategy creation via factory ----------------------------------
def test_calls_create_strategy_factory(
self, mock_model, mock_search, mock_deps
):
_build_system(mock_model, mock_search, mock_deps)
mock_deps["create_strategy"].assert_called_once()
# -- enhanced-contextual-followup strategy --------------------------
def test_enhanced_contextual_followup_creates_delegate(
self, mock_model, mock_search, mock_deps
):
with patch(PATCH_ENHANCED_FOLLOWUP) as m_followup:
m_followup.return_value = _make_strategy_mock()
system = _build_system(
mock_model,
mock_search,
mock_deps,
strategy_name="enhanced-contextual-followup",
)
# create_strategy should have been called for the delegate
mock_deps["create_strategy"].assert_called_once()
delegate_kwargs = mock_deps["create_strategy"].call_args[1]
assert delegate_kwargs["strategy_name"] == "source-based"
# EnhancedContextualFollowUpStrategy should have been instantiated
m_followup.assert_called_once()
assert system.strategy is m_followup.return_value
# -- programmatic_mode warning --------------------------------------
def test_programmatic_mode_logs_warning(
self, mock_model, mock_search, mock_deps
):
with patch("local_deep_research.search_system.logger") as m_logger:
_build_system(
mock_model, mock_search, mock_deps, programmatic_mode=True
)
m_logger.warning.assert_called_once()
assert (
"programmatic mode" in m_logger.warning.call_args[0][0].lower()
)
# ===================================================================
# set_progress_callback tests
# ===================================================================
class TestSetProgressCallback:
"""Tests for set_progress_callback."""
def test_forwards_callback_to_strategy(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
cb = MagicMock(name="my_callback")
system.set_progress_callback(cb)
mock_deps["strategy"].set_progress_callback.assert_called_with(cb)
# ===================================================================
# analyze_topic tests
# ===================================================================
class TestAnalyzeTopic:
"""Tests for analyze_topic."""
def test_generates_search_id_when_not_provided(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
with patch.object(system, "_perform_search", return_value={}) as m_ps:
system.analyze_topic("quantum computing")
# search_id (2nd positional arg) should be a non-empty string
search_id = m_ps.call_args[0][1]
assert isinstance(search_id, str) and len(search_id) > 0
def test_passes_explicit_search_id(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
with patch.object(system, "_perform_search", return_value={}) as m_ps:
system.analyze_topic("q", search_id="my-id-99")
assert m_ps.call_args[0][1] == "my-id-99"
def test_returns_perform_search_result(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
expected = {"current_knowledge": "deep"}
with patch.object(system, "_perform_search", return_value=expected):
result = system.analyze_topic("topic")
assert result is expected
# ===================================================================
# _perform_search tests
# ===================================================================
class TestPerformSearch:
"""Tests for _perform_search."""
def _run_perform_search(
self,
mock_model,
mock_search,
mock_deps,
settings=None,
query="test q",
):
system = _build_system(
mock_model,
mock_search,
mock_deps,
settings_snapshot=settings or {},
)
progress_calls = []
system.progress_callback = lambda m, p, md: progress_calls.append(
(m, p, md)
)
with patch(PATCH_NEWS_CALLBACK):
result = system._perform_search(
query, "sid-1", True, False, "user1"
)
return system, result, progress_calls
def test_extracts_llm_provider_from_settings(
self, mock_model, mock_search, mock_deps
):
settings = {"llm.provider": {"value": "anthropic"}}
_, _, calls = self._run_perform_search(
mock_model, mock_search, mock_deps, settings=settings
)
assert any("anthropic" in str(c) for c in calls)
def test_extracts_llm_model_from_settings(
self, mock_model, mock_search, mock_deps
):
settings = {"llm.model": "claude-3-opus"}
_, _, calls = self._run_perform_search(
mock_model, mock_search, mock_deps, settings=settings
)
assert any("claude-3-opus" in str(c) for c in calls)
def test_extracts_search_tool_from_settings(
self, mock_model, mock_search, mock_deps
):
settings = {"search.tool": {"value": "google"}}
_, _, calls = self._run_perform_search(
mock_model, mock_search, mock_deps, settings=settings
)
assert any("google" in str(c) for c in calls)
def test_calls_progress_callback_with_setup_info(
self, mock_model, mock_search, mock_deps
):
_, _, calls = self._run_perform_search(
mock_model, mock_search, mock_deps
)
# At least two calls: one for LLM info, one for search tool info
assert len(calls) >= 2
phases = [c[2].get("phase") for c in calls]
assert "setup" in phases
def test_calls_strategy_analyze_topic(
self, mock_model, mock_search, mock_deps
):
self._run_perform_search(
mock_model, mock_search, mock_deps, query="my query"
)
mock_deps["strategy"].analyze_topic.assert_called_once_with("my query")
def test_updates_questions_by_iteration(
self, mock_model, mock_search, mock_deps
):
mock_deps["strategy"].questions_by_iteration = ["q1", "q2"]
system, result, _ = self._run_perform_search(
mock_model, mock_search, mock_deps
)
assert system.questions_by_iteration == ["q1", "q2"]
assert result["questions_by_iteration"] == ["q1", "q2"]
def test_result_contains_search_system(
self, mock_model, mock_search, mock_deps
):
system, result, _ = self._run_perform_search(
mock_model, mock_search, mock_deps
)
assert result["search_system"] is system
def test_result_contains_all_links_of_system(
self, mock_model, mock_search, mock_deps
):
mock_deps["strategy"].all_links_of_system = [{"url": "http://a.com"}]
_, result, _ = self._run_perform_search(
mock_model, mock_search, mock_deps
)
assert "all_links_of_system" in result
def test_result_contains_query(self, mock_model, mock_search, mock_deps):
_, result, _ = self._run_perform_search(
mock_model, mock_search, mock_deps, query="deep research"
)
assert result["query"] == "deep research"
def test_avoids_duplicate_links_when_same_object(
self, mock_model, mock_search, mock_deps
):
"""When strategy.all_links_of_system is the SAME object as
system.all_links_of_system, no extend should happen (id check)."""
system = _build_system(mock_model, mock_search, mock_deps)
# Make the strategy share the exact same list object
mock_deps["strategy"].all_links_of_system = system.all_links_of_system
system.all_links_of_system.extend(
[{"url": "http://a.com"}, {"url": "http://b.com"}]
)
system.progress_callback = lambda m, p, md: None
with patch(PATCH_NEWS_CALLBACK):
system._perform_search("q", "sid", True, False, "u")
# If id-check were removed, self-extend would double to 4
assert len(system.all_links_of_system) == 2
def test_extends_links_when_different_objects(
self, mock_model, mock_search, mock_deps
):
"""When strategy.all_links_of_system is a different object,
the links should be extended."""
system = _build_system(mock_model, mock_search, mock_deps)
strategy_links = [{"url": "http://new.com"}]
mock_deps["strategy"].all_links_of_system = strategy_links
system.progress_callback = lambda m, p, md: None
with patch(PATCH_NEWS_CALLBACK):
system._perform_search("q", "sid", True, False, "u")
assert {"url": "http://new.com"} in system.all_links_of_system
def test_news_callback_error_does_not_break_search(
self, mock_model, mock_search, mock_deps
):
system = _build_system(mock_model, mock_search, mock_deps)
system.progress_callback = lambda m, p, md: None
with patch(PATCH_NEWS_CALLBACK, side_effect=Exception("boom")):
# Should not raise
result = system._perform_search("q", "sid", True, False, "u")
assert isinstance(result, dict)