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

514 lines
20 KiB
Python

"""
Tests for DecompositionQuestionGenerator.
Tests cover:
- Initialization with default and custom parameters
- Subject extraction from various question formats
- Compound question splitting on conjunctions
- Article removal from subjects
- LLM response parsing (numbered, bulleted, plain text)
- Error handling (LLM errors, exceptions)
- Default question generation for various topic types
"""
from unittest.mock import Mock
import pytest
from local_deep_research.advanced_search_system.questions.decomposition_question import (
DecompositionQuestionGenerator,
)
class TestDecompositionQuestionGeneratorInit:
"""Tests for DecompositionQuestionGenerator initialization."""
def test_init_with_default_max_subqueries(self):
"""Default max_subqueries is 5."""
mock_model = Mock()
generator = DecompositionQuestionGenerator(mock_model)
assert generator.max_subqueries == 5
def test_init_with_custom_max_subqueries(self):
"""Custom max_subqueries is stored correctly."""
mock_model = Mock()
generator = DecompositionQuestionGenerator(mock_model, max_subqueries=3)
assert generator.max_subqueries == 3
def test_init_stores_model(self):
"""Generator stores the model reference."""
mock_model = Mock()
generator = DecompositionQuestionGenerator(mock_model)
assert generator.model is mock_model
class TestSubjectExtraction:
"""Tests for subject extraction from question formats."""
@pytest.fixture
def mock_model(self):
"""Create mock model that returns valid questions."""
mock = Mock()
mock.invoke.return_value = Mock(
content="What is the definition?\nHow does it work?\nWhat are examples?"
)
return mock
@pytest.fixture
def generator(self, mock_model):
"""Create generator instance."""
return DecompositionQuestionGenerator(mock_model)
def test_extract_subject_from_what_is_question(self, generator):
"""Extract subject from 'what is' question."""
generator.generate_questions("What is machine learning?", "")
# Model was invoked with correct subject
call_args = generator.model.invoke.call_args[0][0]
assert "machine learning" in call_args
def test_extract_subject_from_how_does_question(self, generator):
"""Extract subject from 'how does' question."""
generator.generate_questions("How does deep learning work?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "deep learning work" in call_args
def test_extract_subject_from_why_is_question(self, generator):
"""Extract subject from 'why is' question."""
generator.generate_questions("Why is encryption important?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "encryption important" in call_args
def test_extract_subject_from_who_is_question(self, generator):
"""Extract subject from 'who is' question."""
generator.generate_questions("Who is Alan Turing?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "Alan Turing" in call_args
def test_non_question_uses_full_query(self, generator):
"""Non-question query uses the full query as subject."""
generator.generate_questions("machine learning algorithms", "")
call_args = generator.model.invoke.call_args[0][0]
assert "machine learning algorithms" in call_args
class TestCompoundQuestionSplitting:
"""Tests for splitting compound questions on conjunctions."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(
content="Q1: What is it?\nQ2: How does it work?\nQ3: Examples?"
)
return mock
@pytest.fixture
def generator(self, mock_model):
"""Create generator instance."""
return DecompositionQuestionGenerator(mock_model)
def test_split_on_and_conjunction(self, generator):
"""Split compound question at ' and '."""
generator.generate_questions("What is Python and how is it used?", "")
call_args = generator.model.invoke.call_args[0][0]
# Should extract "Python" before " and "
assert "Python" in call_args
def test_split_on_or_conjunction(self, generator):
"""Split compound question at ' or '."""
generator.generate_questions(
"What is Java or C++ better for games?", ""
)
call_args = generator.model.invoke.call_args[0][0]
assert "Java" in call_args
def test_split_on_but_conjunction(self, generator):
"""Split compound question at ' but '."""
generator.generate_questions("What is fast but easy to learn?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "fast" in call_args
def test_split_on_when_conjunction(self, generator):
"""Split compound question at ' when '."""
generator.generate_questions(
"What is Python when used for web development?", ""
)
call_args = generator.model.invoke.call_args[0][0]
assert "Python" in call_args
class TestArticleRemoval:
"""Tests for removing articles from subjects."""
@pytest.fixture
def mock_model(self):
"""Create mock model."""
mock = Mock()
mock.invoke.return_value = Mock(content="Q1\nQ2\nQ3")
return mock
@pytest.fixture
def generator(self, mock_model):
"""Create generator instance."""
return DecompositionQuestionGenerator(mock_model)
def test_remove_article_a(self, generator):
"""Remove 'a' article from subject."""
generator.generate_questions("What is a neural network?", "")
call_args = generator.model.invoke.call_args[0][0]
# Should not contain "a neural network", just "neural network"
assert "neural network" in call_args
def test_remove_article_an(self, generator):
"""Remove 'an' article from subject."""
generator.generate_questions("What is an algorithm?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "algorithm" in call_args
def test_remove_article_the(self, generator):
"""Remove 'the' article from subject."""
generator.generate_questions("What is the internet?", "")
call_args = generator.model.invoke.call_args[0][0]
assert "internet" in call_args
class TestLLMResponseParsing:
"""Tests for parsing different LLM response formats."""
@pytest.fixture
def generator(self):
"""Create generator with mock model."""
mock_model = Mock()
return DecompositionQuestionGenerator(mock_model)
def test_parse_numbered_response(self, generator):
"""Parse numbered list format (1. 2. 3.)."""
generator.model.invoke.return_value = Mock(
content="1. What is the definition?\n2. How does it work?\n3. What are examples?"
)
result = generator.generate_questions("test topic", "")
# Lines starting with "1. " etc. are skipped in first pass, but content is extracted
assert len(result) >= 0 # May use fallback depending on parsing
def test_parse_bulleted_response(self, generator):
"""Parse bulleted list format (-, *, •)."""
generator.model.invoke.return_value = Mock(
content="- What is the definition of topic?\n* How does topic work?\n• What are topic examples?"
)
result = generator.generate_questions("test topic", "")
assert len(result) >= 0
def test_parse_plain_text_response(self, generator):
"""Parse plain text questions (one per line, no bullets)."""
generator.model.invoke.return_value = Mock(
content="What is the definition of topic?\nHow does topic work?\nWhat are topic examples?"
)
result = generator.generate_questions("test topic", "")
assert len(result) == 3
assert "What is the definition of topic?" in result
def test_handle_response_with_content_attribute(self, generator):
"""Handle response object with .content attribute."""
mock_response = Mock()
mock_response.content = (
"Question one here?\nQuestion two here?\nQuestion three here?"
)
generator.model.invoke.return_value = mock_response
result = generator.generate_questions("test", "")
assert len(result) == 3
def test_handle_string_response(self, generator):
"""Handle plain string response (no .content attribute)."""
generator.model.invoke.return_value = (
"Question one here?\nQuestion two here?\nQuestion three here?"
)
result = generator.generate_questions("test", "")
assert len(result) == 3
def test_filter_short_lines(self, generator):
"""Filter out lines shorter than 10 characters."""
generator.model.invoke.return_value = Mock(
content="Short\nThis is a valid question longer than ten chars?\nOK"
)
result = generator.generate_questions("test", "")
assert len(result) == 1
assert "This is a valid question" in result[0]
def test_skip_empty_lines(self, generator):
"""Skip empty lines in response."""
generator.model.invoke.return_value = Mock(
content="First valid question here?\n\n\nSecond valid question here?"
)
result = generator.generate_questions("test", "")
assert len(result) == 2
def test_respect_max_subqueries_limit(self, generator):
"""Respect max_subqueries limit."""
generator.max_subqueries = 2
generator.model.invoke.return_value = Mock(
content="Question one here?\nQuestion two here?\nQuestion three here?\nQuestion four here?"
)
result = generator.generate_questions("test", "")
assert len(result) <= 2
class TestLLMErrorHandling:
"""Tests for handling LLM errors and fallbacks."""
@pytest.fixture
def generator(self):
"""Create generator with mock model."""
mock_model = Mock()
return DecompositionQuestionGenerator(mock_model)
def test_handle_no_language_models_error(self, generator):
"""Fall back to defaults when 'No language models are available'."""
generator.model.invoke.return_value = Mock(
content="No language models are available. Please install Ollama."
)
result = generator.generate_questions("What is Python?", "")
# Should return default questions
assert len(result) > 0
assert any("Python" in q for q in result)
def test_handle_please_install_ollama_error(self, generator):
"""Fall back to defaults when 'Please install Ollama'."""
generator.model.invoke.return_value = Mock(
content="Please install Ollama to use local models."
)
result = generator.generate_questions("What is Python?", "")
assert len(result) > 0
def test_handle_llm_exception(self, generator):
"""Fall back to defaults when LLM raises exception."""
generator.model.invoke.side_effect = RuntimeError("Connection failed")
result = generator.generate_questions("What is Python?", "")
# Should catch exception and return default questions
assert len(result) > 0
assert any("Python" in q for q in result)
def test_simplified_prompt_fallback(self, generator):
"""Use simplified prompt when first attempt yields no results."""
# First call returns unparseable content, second returns valid
generator.model.invoke.side_effect = [
Mock(
content="*\n-\n•"
), # Only formatting chars, no valid questions
Mock(
content="1. Valid question here?\n2. Another valid one?\n3. Third question?"
),
]
generator.generate_questions("test topic", "")
# Should have called invoke twice
assert generator.model.invoke.call_count == 2
def test_fallback_to_defaults_after_both_prompts_fail(self, generator):
"""Fall back to defaults when both prompts fail."""
generator.model.invoke.side_effect = [
Mock(content=""), # Empty first response
Mock(content=""), # Empty second response
]
result = generator.generate_questions("What is AI?", "")
# Should return default questions
assert len(result) > 0
class TestDefaultQuestionGeneration:
"""Tests for _generate_default_questions method."""
@pytest.fixture
def generator(self):
"""Create generator with mock model."""
mock_model = Mock()
return DecompositionQuestionGenerator(mock_model)
def test_csrf_special_case(self, generator):
"""Generate CSRF-specific questions for CSRF queries."""
result = generator._generate_default_questions("What is CSRF?")
assert any("CSRF" in q for q in result)
assert any("Cross-Site Request Forgery" in q for q in result)
def test_csrf_full_name(self, generator):
"""Generate CSRF-specific questions for full name."""
result = generator._generate_default_questions(
"What is cross-site request forgery?"
)
assert any("CSRF" in q for q in result)
def test_security_topic_questions(self, generator):
"""Generate security-focused questions for security topics."""
result = generator._generate_default_questions(
"What is SQL injection vulnerability?"
)
assert any(
"vulnerability" in q.lower() or "attack" in q.lower()
for q in result
)
def test_programming_topic_questions(self, generator):
"""Generate programming-focused questions for programming topics."""
result = generator._generate_default_questions(
"Python programming language"
)
assert any(
"features" in q.lower() or "advantages" in q.lower() for q in result
)
def test_short_subject_questions(self, generator):
"""Generate appropriate questions for short subjects (1-2 words)."""
result = generator._generate_default_questions("AI")
assert result[0] == "What is AI?"
assert any("characteristics" in q for q in result)
def test_generic_topic_questions(self, generator):
"""Generate generic questions for unrecognized topics."""
result = generator._generate_default_questions(
"quantum entanglement in physics research"
)
assert any("definition" in q.lower() for q in result)
assert any(
"components" in q.lower() or "features" in q.lower() for q in result
)
def test_empty_query_questions(self, generator):
"""Generate generic questions for empty query."""
result = generator._generate_default_questions("")
assert len(result) > 0
assert any("definition" in q.lower() for q in result)
def test_default_respects_max_subqueries(self, generator):
"""Default questions respect max_subqueries limit."""
generator.max_subqueries = 2
result = generator._generate_default_questions("What is Python?")
assert len(result) <= 2
class TestContextHandling:
"""Tests for context parameter handling."""
@pytest.fixture
def generator(self):
"""Create generator with mock model."""
mock_model = Mock()
mock_model.invoke.return_value = Mock(
content="Q1: First question?\nQ2: Second question?\nQ3: Third?"
)
return DecompositionQuestionGenerator(mock_model)
def test_context_included_in_prompt(self, generator):
"""Context is included in the LLM prompt."""
context = "This is relevant context information."
generator.generate_questions("test query", context)
call_args = generator.model.invoke.call_args[0][0]
assert "This is relevant context information" in call_args
def test_long_context_truncated(self, generator):
"""Long context is truncated to 2000 characters."""
long_context = "A" * 5000
generator.generate_questions("test query", long_context)
call_args = generator.model.invoke.call_args[0][0]
# Context should be truncated - the prompt won't contain 5000 A's
assert call_args.count("A") <= 2000
def test_empty_context_handled(self, generator):
"""Empty context is handled gracefully."""
result = generator.generate_questions("test query", "")
assert result is not None
class TestIntegration:
"""Integration tests for complete workflows."""
@pytest.fixture
def mock_model(self):
"""Create mock model with realistic responses."""
mock = Mock()
mock.invoke.return_value = Mock(
content="""What is the definition of machine learning?
How does machine learning differ from traditional programming?
What are common applications of machine learning?
What are the main types of machine learning algorithms?"""
)
return mock
@pytest.fixture
def generator(self, mock_model):
"""Create generator instance."""
return DecompositionQuestionGenerator(mock_model)
def test_full_workflow_simple_query(self, generator):
"""Test complete workflow with simple query."""
result = generator.generate_questions("machine learning", "")
assert isinstance(result, list)
assert len(result) > 0
assert all(isinstance(q, str) for q in result)
def test_full_workflow_question_query(self, generator):
"""Test complete workflow with question-format query."""
result = generator.generate_questions(
"What is machine learning and how does it work?", ""
)
assert isinstance(result, list)
assert len(result) > 0
def test_full_workflow_with_context(self, generator):
"""Test complete workflow with context provided."""
context = "Machine learning is a subset of artificial intelligence."
result = generator.generate_questions("machine learning", context)
assert isinstance(result, list)
assert len(result) > 0
class TestEdgeCases:
"""Tests for edge cases and boundary conditions."""
@pytest.fixture
def generator(self):
"""Create generator with mock model."""
mock_model = Mock()
mock_model.invoke.return_value = Mock(content="Valid question here?")
return DecompositionQuestionGenerator(mock_model)
def test_whitespace_only_query(self, generator):
"""Handle whitespace-only query."""
result = generator.generate_questions(" ", "")
assert isinstance(result, list)
def test_special_characters_in_query(self, generator):
"""Handle special characters in query."""
result = generator.generate_questions("What is C++ & C#?", "")
assert isinstance(result, list)
def test_unicode_in_query(self, generator):
"""Handle unicode characters in query."""
result = generator.generate_questions("What is 日本語?", "")
assert isinstance(result, list)
def test_very_long_query(self, generator):
"""Handle very long query."""
long_query = "What is " + "very " * 100 + "long query?"
result = generator.generate_questions(long_query, "")
assert isinstance(result, list)
def test_query_with_newlines(self, generator):
"""Handle query with newline characters."""
result = generator.generate_questions("What is\nmachine\nlearning?", "")
assert isinstance(result, list)
def test_max_subqueries_zero(self, generator):
"""Handle max_subqueries set to zero."""
generator.max_subqueries = 0
generator.model.invoke.return_value = Mock(content="Q1?\nQ2?\nQ3?")
result = generator.generate_questions("test", "")
assert len(result) == 0
def test_max_subqueries_one(self, generator):
"""Handle max_subqueries set to one."""
generator.max_subqueries = 1
generator.model.invoke.return_value = Mock(
content="First question?\nSecond question?\nThird question?"
)
result = generator.generate_questions("test", "")
assert len(result) == 1