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

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")