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

314 lines
10 KiB
Python

"""
Tests for news/utils/topic_generator.py
Tests cover:
- Topic generation with mocked LLM
- Topic validation and cleaning
- Edge cases: empty input, duplicates, invalid topics
"""
from unittest.mock import patch, MagicMock
class TestValidateTopics:
"""Tests for the _validate_topics function."""
def test_removes_empty_topics(self):
"""Test that empty topics are removed."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = ["valid", "", " ", "another", None]
# Filter out None before passing
topics = [t for t in topics if t is not None]
result = _validate_topics(topics, 5)
assert "valid" in result
assert "another" in result
assert "" not in result
def test_removes_short_topics(self):
"""Test that topics shorter than 2 chars are removed."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = ["a", "ab", "abc", "valid topic"]
result = _validate_topics(topics, 5)
assert "a" not in result
assert "ab" in result
assert "abc" in result
assert "valid topic" in result
def test_removes_long_topics(self):
"""Test that topics longer than 30 chars are removed."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
long_topic = "x" * 31
short_topic = "x" * 30
topics = [long_topic, short_topic, "normal"]
result = _validate_topics(topics, 5)
assert long_topic not in result
assert "x" * 30 in result
assert "normal" in result
def test_removes_case_insensitive_duplicates(self):
"""Test that duplicates are removed case-insensitively."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = ["Technology", "technology", "TECHNOLOGY", "Other"]
result = _validate_topics(topics, 5)
# Should only have one version of technology
tech_count = sum(1 for t in result if "technolog" in t.lower())
assert tech_count == 1
assert "other" in result
def test_respects_max_topics_limit(self):
"""Test that max_topics limit is enforced."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = ["one", "two", "three", "four", "five", "six"]
result = _validate_topics(topics, 3)
assert len(result) <= 3
def test_converts_to_lowercase(self):
"""Test that topics are converted to lowercase."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = ["UPPERCASE", "MixedCase", "lowercase"]
result = _validate_topics(topics, 5)
for topic in result:
assert topic == topic.lower()
def test_strips_whitespace(self):
"""Test that whitespace is stripped from topics."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
topics = [" spaced ", "\ttabbed\t", "normal"]
result = _validate_topics(topics, 5)
for topic in result:
assert topic == topic.strip()
def test_empty_input_returns_no_valid_topics_message(self):
"""Test that empty input returns failure indicator."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
result = _validate_topics([], 5)
assert result == ["[No valid topics]"]
def test_all_invalid_returns_failure_message(self):
"""Test that all invalid topics returns failure indicator."""
from local_deep_research.news.utils.topic_generator import (
_validate_topics,
)
# All topics are too short
topics = ["a", "b", "c"]
result = _validate_topics(topics, 5)
assert result == ["[No valid topics]"]
class TestGenerateTopics:
"""Tests for the generate_topics function."""
@patch("local_deep_research.news.utils.topic_generator._generate_with_llm")
def test_returns_llm_topics_when_successful(self, mock_llm_gen):
"""Test that LLM topics are returned when generation succeeds."""
from local_deep_research.news.utils.topic_generator import (
generate_topics,
)
mock_llm_gen.return_value = ["tech", "innovation", "startup"]
result = generate_topics(
query="tech news", findings="A startup launched a new product"
)
assert "tech" in result
assert "innovation" in result
assert "startup" in result
@patch("local_deep_research.news.utils.topic_generator._generate_with_llm")
def test_returns_failure_message_when_llm_fails(self, mock_llm_gen):
"""Test that failure message is returned when LLM fails."""
from local_deep_research.news.utils.topic_generator import (
generate_topics,
)
mock_llm_gen.return_value = []
result = generate_topics(query="test", findings="test")
assert "[topic generation failed]" in result
class TestGenerateWithLLM:
"""Tests for the _generate_with_llm function."""
@patch("local_deep_research.config.llm_config.get_llm")
def test_parses_json_array_response(self, mock_get_llm):
"""Test parsing of JSON array from LLM."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '["topic1", "topic2", "topic3"]'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
result = _generate_with_llm("query", "findings", "category", 5)
assert "topic1" in result
assert "topic2" in result
assert "topic3" in result
@patch("local_deep_research.config.llm_config.get_llm")
def test_handles_json_with_markdown_code_block(self, mock_get_llm):
"""Test handling of JSON wrapped in markdown code block."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '```json\n["topic1", "topic2"]\n```'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
result = _generate_with_llm("query", "findings", "category", 5)
assert "topic1" in result
assert "topic2" in result
@patch("local_deep_research.config.llm_config.get_llm")
def test_handles_comma_separated_fallback(self, mock_get_llm):
"""Test fallback parsing of comma-separated topics."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = "topic one, topic two, topic three"
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
result = _generate_with_llm("query", "findings", "category", 5)
assert "topic one" in result
assert "topic two" in result
@patch("local_deep_research.config.llm_config.get_llm")
def test_respects_max_topics(self, mock_get_llm):
"""Test that max_topics limit is respected."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '["t1", "t2", "t3", "t4", "t5", "t6", "t7"]'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
result = _generate_with_llm("query", "findings", "category", 3)
assert len(result) <= 3
@patch("local_deep_research.config.llm_config.get_llm")
def test_filters_long_topics(self, mock_get_llm):
"""Test that topics longer than 30 chars are filtered."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
long_topic = "x" * 35
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = f'["short", "{long_topic}"]'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
result = _generate_with_llm("query", "findings", "category", 5)
assert "short" in result
assert long_topic not in result
@patch("local_deep_research.config.llm_config.get_llm")
def test_handles_llm_exception(self, mock_get_llm):
"""Test graceful handling of LLM exceptions."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_get_llm.side_effect = Exception("LLM error")
result = _generate_with_llm("query", "findings", "category", 5)
assert result == []
@patch("local_deep_research.config.llm_config.get_llm")
def test_uses_medium_temperature(self, mock_get_llm):
"""Test that medium temperature is used for topic diversity."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '["topic"]'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
_generate_with_llm("query", "findings", "category", 5)
mock_get_llm.assert_called_once_with(
temperature=0.5, settings_snapshot=None
)
@patch("local_deep_research.config.llm_config.get_llm")
def test_truncates_long_query_in_prompt(self, mock_get_llm):
"""Test that long queries are truncated in prompt."""
from local_deep_research.news.utils.topic_generator import (
_generate_with_llm,
)
mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '["topic"]'
mock_llm.invoke.return_value = mock_response
mock_get_llm.return_value = mock_llm
long_query = "x" * 1000
_generate_with_llm(long_query, "findings", "category", 5)
# The prompt should be invoked with truncated query
call_args = mock_llm.invoke.call_args
prompt = call_args[0][0]
# Query should be truncated to 500 chars
assert "x" * 501 not in prompt