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
220 lines
8.2 KiB
Python
220 lines
8.2 KiB
Python
"""
|
|
Tests for research_service helper functions.
|
|
|
|
Tests cover:
|
|
- _parse_research_metadata: dict/JSON/invalid input handling
|
|
- get_citation_formatter: citation mode mapping from settings
|
|
- export_report_to_memory: exporter registry integration
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.web.services.research_service import (
|
|
_parse_research_metadata,
|
|
export_report_to_memory,
|
|
get_citation_formatter,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _parse_research_metadata
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseResearchMetadata:
|
|
"""Tests for _parse_research_metadata helper."""
|
|
|
|
def test_returns_dict_as_is(self):
|
|
"""A plain dict input is returned unchanged (as a copy)."""
|
|
original = {"key": "value", "num": 42}
|
|
result = _parse_research_metadata(original)
|
|
assert result == original
|
|
# Should be a copy, not the same object
|
|
assert result is not original
|
|
|
|
def test_parses_json_string(self):
|
|
"""A valid JSON string is parsed into a dict."""
|
|
json_str = '{"key": "value", "num": 42}'
|
|
result = _parse_research_metadata(json_str)
|
|
assert result == {"key": "value", "num": 42}
|
|
|
|
def test_invalid_json_returns_empty_dict(self):
|
|
"""An invalid JSON string returns an empty dict."""
|
|
result = _parse_research_metadata("not valid json {{{")
|
|
assert result == {}
|
|
|
|
def test_none_returns_empty_dict(self):
|
|
"""None input returns an empty dict."""
|
|
result = _parse_research_metadata(None)
|
|
assert result == {}
|
|
|
|
def test_integer_returns_empty_dict(self):
|
|
"""An integer input returns an empty dict."""
|
|
result = _parse_research_metadata(999)
|
|
assert result == {}
|
|
|
|
def test_empty_string_returns_empty_dict(self):
|
|
"""An empty string (invalid JSON) returns an empty dict."""
|
|
result = _parse_research_metadata("")
|
|
assert result == {}
|
|
|
|
def test_preserves_nested_dict_structure(self):
|
|
"""Nested dict structures survive the round-trip."""
|
|
nested = {
|
|
"outer": {"inner": [1, 2, 3]},
|
|
"list": [{"a": 1}, {"b": 2}],
|
|
}
|
|
result = _parse_research_metadata(nested)
|
|
assert result == nested
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_citation_formatter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetCitationFormatter:
|
|
"""Tests for get_citation_formatter with mocked settings."""
|
|
|
|
@patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot",
|
|
return_value="number_hyperlinks",
|
|
)
|
|
def test_number_hyperlinks_setting(self, mock_get_setting):
|
|
"""'number_hyperlinks' maps to CitationMode.NUMBER_HYPERLINKS."""
|
|
from local_deep_research.text_optimization import CitationMode
|
|
|
|
formatter = get_citation_formatter()
|
|
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS
|
|
|
|
@patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot",
|
|
return_value="domain_hyperlinks",
|
|
)
|
|
def test_domain_hyperlinks_setting(self, mock_get_setting):
|
|
"""'domain_hyperlinks' maps to CitationMode.DOMAIN_HYPERLINKS."""
|
|
from local_deep_research.text_optimization import CitationMode
|
|
|
|
formatter = get_citation_formatter()
|
|
assert formatter.mode == CitationMode.DOMAIN_HYPERLINKS
|
|
|
|
@patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot",
|
|
return_value="no_hyperlinks",
|
|
)
|
|
def test_no_hyperlinks_setting(self, mock_get_setting):
|
|
"""'no_hyperlinks' maps to CitationMode.NO_HYPERLINKS."""
|
|
from local_deep_research.text_optimization import CitationMode
|
|
|
|
formatter = get_citation_formatter()
|
|
assert formatter.mode == CitationMode.NO_HYPERLINKS
|
|
|
|
@patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot",
|
|
return_value="totally_unknown_format",
|
|
)
|
|
def test_unknown_setting_defaults_to_number_hyperlinks(
|
|
self, mock_get_setting
|
|
):
|
|
"""An unrecognised setting value falls back to NUMBER_HYPERLINKS."""
|
|
from local_deep_research.text_optimization import CitationMode
|
|
|
|
formatter = get_citation_formatter()
|
|
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS
|
|
|
|
@patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot",
|
|
return_value="number_hyperlinks",
|
|
)
|
|
def test_default_setting_returns_number_hyperlinks(self, mock_get_setting):
|
|
"""When the setting returns its default, formatter uses NUMBER_HYPERLINKS."""
|
|
from local_deep_research.text_optimization import CitationMode
|
|
|
|
formatter = get_citation_formatter()
|
|
# Verify the setting was requested with the correct default
|
|
mock_get_setting.assert_called_once_with(
|
|
"report.citation_format", "number_hyperlinks"
|
|
)
|
|
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# export_report_to_memory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExportReportToMemory:
|
|
"""Tests for export_report_to_memory with mocked exporter registry."""
|
|
|
|
@patch("local_deep_research.exporters.ExportOptions")
|
|
@patch("local_deep_research.exporters.ExporterRegistry")
|
|
def test_calls_get_exporter_with_lowercase_format(
|
|
self, mock_registry, mock_options_cls
|
|
):
|
|
"""The format string is lowered before being passed to get_exporter."""
|
|
mock_exporter = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.content = b"data"
|
|
mock_result.filename = "report.pdf"
|
|
mock_result.mimetype = "application/pdf"
|
|
mock_exporter.export.return_value = mock_result
|
|
mock_registry.get_exporter.return_value = mock_exporter
|
|
|
|
export_report_to_memory("# Hello", "PDF", title="My Report")
|
|
|
|
mock_registry.get_exporter.assert_called_once_with("pdf")
|
|
|
|
@patch("local_deep_research.exporters.ExportOptions")
|
|
@patch("local_deep_research.exporters.ExporterRegistry")
|
|
def test_raises_value_error_for_unsupported_format(
|
|
self, mock_registry, mock_options_cls
|
|
):
|
|
"""When ExporterRegistry.get_exporter returns None, ValueError is raised."""
|
|
mock_registry.get_exporter.return_value = None
|
|
mock_registry.get_available_formats.return_value = ["pdf", "odt"]
|
|
|
|
with pytest.raises(ValueError, match="Unsupported export format"):
|
|
export_report_to_memory("# Hello", "bmp")
|
|
|
|
@patch("local_deep_research.exporters.ExportOptions")
|
|
@patch("local_deep_research.exporters.ExporterRegistry")
|
|
def test_returns_tuple_of_content_filename_mimetype(
|
|
self, mock_registry, mock_options_cls
|
|
):
|
|
"""Successful export returns (content, filename, mimetype)."""
|
|
mock_exporter = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.content = b"pdf-bytes"
|
|
mock_result.filename = "report.pdf"
|
|
mock_result.mimetype = "application/pdf"
|
|
mock_exporter.export.return_value = mock_result
|
|
mock_registry.get_exporter.return_value = mock_exporter
|
|
|
|
content, filename, mimetype = export_report_to_memory(
|
|
"# Report", "pdf", title="Test"
|
|
)
|
|
|
|
assert content == b"pdf-bytes"
|
|
assert filename == "report.pdf"
|
|
assert mimetype == "application/pdf"
|
|
|
|
@patch("local_deep_research.exporters.ExportOptions")
|
|
@patch("local_deep_research.exporters.ExporterRegistry")
|
|
def test_normalizes_format_to_lowercase(
|
|
self, mock_registry, mock_options_cls
|
|
):
|
|
"""Mixed-case format strings are normalised before lookup."""
|
|
mock_exporter = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.content = b""
|
|
mock_result.filename = "report.odt"
|
|
mock_result.mimetype = "application/vnd.oasis.opendocument.text"
|
|
mock_exporter.export.return_value = mock_result
|
|
mock_registry.get_exporter.return_value = mock_exporter
|
|
|
|
export_report_to_memory("# Hello", "OdT")
|
|
|
|
mock_registry.get_exporter.assert_called_once_with("odt")
|