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

323 lines
11 KiB
Python

"""
Gap tests for search_system.AdvancedSearchSystem.
Focuses on testable pure logic:
- Settings extraction for max_iterations / questions_per_iteration
(dict-with-value vs plain value vs missing -> default)
- _perform_search settings extraction for llm_provider, llm_model, search_tool
- close() resource cleanup
- set_progress_callback propagation
- analyze_topic generates search_id when not provided
- all_links_of_system dedup (same-object identity check)
"""
from unittest.mock import Mock, patch
import pytest
# ---------------------------------------------------------------------------
# Factory fixture for AdvancedSearchSystem
# ---------------------------------------------------------------------------
@pytest.fixture
def make_system():
"""Factory fixture returning a function that builds an
AdvancedSearchSystem with a mocked strategy.
Returns ``(system, mock_strategy)`` so tests can inspect or mutate
the strategy mock and assert on the system.
"""
def _factory(
settings_snapshot=None, strategy_name="source-based", **kwargs
):
mock_llm = Mock()
mock_search = Mock()
mock_strategy = Mock()
mock_strategy.questions_by_iteration = {}
mock_strategy.all_links_of_system = []
mock_strategy.set_progress_callback = Mock()
mock_strategy.analyze_topic = Mock(
return_value={
"findings": [],
"current_knowledge": "",
}
)
with patch(
"local_deep_research.search_system_factory.create_strategy",
return_value=mock_strategy,
):
from local_deep_research.search_system import AdvancedSearchSystem
system = AdvancedSearchSystem(
llm=mock_llm,
search=mock_search,
strategy_name=strategy_name,
settings_snapshot=settings_snapshot,
**kwargs,
)
return system, mock_strategy
return _factory
# ---------------------------------------------------------------------------
# __init__ -- max_iterations extraction
# ---------------------------------------------------------------------------
class TestMaxIterationsExtraction:
"""AdvancedSearchSystem.__init__ reads max_iterations from settings."""
def test_explicit_value_takes_precedence(self, make_system):
system, _ = make_system(
settings_snapshot={"search.iterations": {"value": 10}},
max_iterations=5,
)
assert system.max_iterations == 5
def test_dict_with_value_key(self, make_system):
system, _ = make_system(
settings_snapshot={"search.iterations": {"value": 7}},
)
assert system.max_iterations == 7
def test_plain_value(self, make_system):
system, _ = make_system(
settings_snapshot={"search.iterations": 4},
)
assert system.max_iterations == 4
def test_missing_defaults_to_1(self, make_system):
system, _ = make_system(settings_snapshot={})
assert system.max_iterations == 1
def test_none_snapshot_defaults_to_1(self, make_system):
system, _ = make_system(settings_snapshot=None)
assert system.max_iterations == 1
# ---------------------------------------------------------------------------
# __init__ -- questions_per_iteration extraction
# ---------------------------------------------------------------------------
class TestQuestionsPerIterationExtraction:
"""AdvancedSearchSystem.__init__ reads questions_per_iteration from settings."""
def test_explicit_value_takes_precedence(self, make_system):
system, _ = make_system(
settings_snapshot={"search.questions_per_iteration": {"value": 10}},
questions_per_iteration=2,
)
assert system.questions_per_iteration == 2
def test_dict_with_value_key(self, make_system):
system, _ = make_system(
settings_snapshot={"search.questions_per_iteration": {"value": 5}},
)
assert system.questions_per_iteration == 5
def test_plain_value(self, make_system):
system, _ = make_system(
settings_snapshot={"search.questions_per_iteration": 8},
)
assert system.questions_per_iteration == 8
def test_missing_defaults_to_3(self, make_system):
system, _ = make_system(settings_snapshot={})
assert system.questions_per_iteration == 3
# ---------------------------------------------------------------------------
# _perform_search -- settings extraction for progress messages
# ---------------------------------------------------------------------------
class TestPerformSearchSettingsExtraction:
"""_perform_search extracts provider/model/tool from settings snapshot."""
def _run_perform_search(self, system):
"""Run _perform_search capturing progress callback calls."""
calls = []
system.progress_callback = lambda msg, pct, meta: calls.append(
(msg, pct, meta)
)
with patch(
"local_deep_research.news.core.search_integration.NewsSearchCallback",
):
system._perform_search("q", "id1", True, False, "user1")
return calls
def test_extracts_from_dict_with_value(self, make_system):
system, _ = make_system(
settings_snapshot={
"llm.provider": {"value": "openai"},
"llm.model": {"value": "gpt-4"},
"search.tool": {"value": "google"},
},
)
calls = self._run_perform_search(system)
assert any("openai" in c[0] or "gpt-4" in c[0] for c in calls)
def test_extracts_plain_values(self, make_system):
system, _ = make_system(
settings_snapshot={
"llm.provider": "anthropic",
"llm.model": "claude",
"search.tool": "searxng",
},
)
calls = self._run_perform_search(system)
assert any("anthropic" in c[0] or "claude" in c[0] for c in calls)
def test_missing_settings_use_unknown(self, make_system):
system, _ = make_system(settings_snapshot={})
calls = self._run_perform_search(system)
assert any("unknown" in c[0] for c in calls)
def test_search_tool_default_from_dict(self, make_system):
"""search.tool dict without 'value' key defaults to 'searxng'."""
system, _ = make_system(
settings_snapshot={"search.tool": {"other": "x"}},
)
calls = self._run_perform_search(system)
# Should use the .get("value", "searxng") default
assert any("searxng" in c[0] for c in calls)
# ---------------------------------------------------------------------------
# all_links_of_system -- identity-based dedup
# ---------------------------------------------------------------------------
class TestAllLinksDedup:
"""_perform_search avoids extending all_links when objects are same."""
def test_same_object_no_duplication(self, make_system):
"""When strategy.all_links_of_system IS system.all_links_of_system,
no extension should happen (prevents issue #301)."""
system, mock_strategy = make_system(settings_snapshot={})
# Make them the same object
mock_strategy.all_links_of_system = system.all_links_of_system
system.all_links_of_system.append({"link": "https://a.com"})
with patch(
"local_deep_research.news.core.search_integration.NewsSearchCallback",
):
system._perform_search("q", "id1", True, False, "user1")
# Should NOT be doubled
assert len(system.all_links_of_system) == 1
def test_different_objects_extends(self, make_system):
"""When they are different objects, links should be extended."""
system, mock_strategy = make_system(settings_snapshot={})
mock_strategy.all_links_of_system = [{"link": "https://b.com"}]
with patch(
"local_deep_research.news.core.search_integration.NewsSearchCallback",
):
system._perform_search("q", "id1", True, False, "user1")
assert {"link": "https://b.com"} in system.all_links_of_system
# ---------------------------------------------------------------------------
# close() -- resource cleanup
# ---------------------------------------------------------------------------
class TestClose:
"""AdvancedSearchSystem.close cascades to strategy."""
def test_close_calls_safe_close_on_strategy(self, make_system):
system, mock_strategy = make_system(settings_snapshot={})
with patch(
"local_deep_research.utilities.resource_utils.safe_close"
) as mock_safe_close:
system.close()
mock_safe_close.assert_called_once_with(
mock_strategy, "search strategy"
)
def test_close_without_strategy_attribute(self, make_system):
"""close() should not raise if strategy attribute is missing."""
system, _ = make_system(settings_snapshot={})
del system.strategy
with patch(
"local_deep_research.utilities.resource_utils.safe_close"
) as mock_safe_close:
system.close() # should not raise
mock_safe_close.assert_not_called()
# ---------------------------------------------------------------------------
# set_progress_callback
# ---------------------------------------------------------------------------
class TestSetProgressCallback:
def test_callback_set_on_system_and_strategy(self, make_system):
system, mock_strategy = make_system(settings_snapshot={})
cb = Mock()
system.set_progress_callback(cb)
assert system.progress_callback is cb
mock_strategy.set_progress_callback.assert_called_with(cb)
# ---------------------------------------------------------------------------
# analyze_topic -- search_id auto-generation
# ---------------------------------------------------------------------------
class TestAnalyzeTopicSearchId:
def test_generates_uuid_when_search_id_is_none(self, make_system):
system, _ = make_system(settings_snapshot={})
with patch.object(
system, "_perform_search", return_value={}
) as mock_ps:
system.analyze_topic("test query")
args = mock_ps.call_args
search_id = args[0][1] # second positional arg
# Should be a valid UUID string (36 chars with hyphens)
assert len(search_id) == 36
assert search_id.count("-") == 4
def test_uses_provided_search_id(self, make_system):
system, _ = make_system(settings_snapshot={})
with patch.object(
system, "_perform_search", return_value={}
) as mock_ps:
system.analyze_topic("test query", search_id="custom-id")
args = mock_ps.call_args
search_id = args[0][1]
assert search_id == "custom-id"
# ---------------------------------------------------------------------------
# programmatic_mode flag
# ---------------------------------------------------------------------------
class TestProgrammaticMode:
def test_programmatic_mode_stored(self, make_system):
system, _ = make_system(settings_snapshot={}, programmatic_mode=True)
assert system.programmatic_mode is True
def test_non_programmatic_mode_default(self, make_system):
system, _ = make_system(settings_snapshot={})
assert system.programmatic_mode is False