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
323 lines
11 KiB
Python
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
|