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

508 lines
17 KiB
Python

"""
Tests for research_service error handling.
Tests cover:
- Error handler functionality
- Termination handling
"""
from unittest.mock import Mock, MagicMock, patch
class TestErrorHandler:
"""Tests for error handler functionality."""
@patch(
"local_deep_research.error_handling.report_generator.ErrorReportGenerator"
)
def test_error_handler_enhanced_report_generation(
self, mock_generator_class
):
"""Error handler generates enhanced error reports."""
mock_generator = Mock()
mock_generator.generate_error_report.return_value = (
"# Error Report\n\nResearch failed due to LLM timeout."
)
mock_generator_class.return_value = mock_generator
from local_deep_research.error_handling.report_generator import (
ErrorReportGenerator,
)
generator = ErrorReportGenerator(llm=None)
report = generator.generate_error_report(
error_message="Timeout error",
query="test query",
partial_results=None,
search_iterations=0,
research_id=123,
)
assert "Error Report" in report
@patch(
"local_deep_research.web.services.research_service.get_user_db_session"
)
def test_error_handler_database_update_on_failure(self, mock_get_session):
"""Error handler updates database on failure."""
mock_session = MagicMock()
mock_research = Mock()
mock_research.status = "in_progress"
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_research
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=False)
mock_get_session.return_value = mock_session
# Simulate status update
mock_research.status = "failed"
mock_session.commit()
assert mock_research.status == "failed"
mock_session.commit.assert_called()
def test_error_handler_status_transition_in_progress_to_failed(self):
"""Error handler transitions status from in_progress to failed."""
error_occurred = True
if error_occurred:
final_status = "failed"
else:
final_status = "completed"
assert final_status == "failed"
def test_error_handler_status_transition_in_progress_to_suspended(self):
"""Error handler transitions status to suspended on termination."""
termination_requested = True
if termination_requested:
final_status = "suspended"
else:
final_status = "failed"
assert final_status == "suspended"
def test_error_handler_error_message_formatting(self):
"""Error handler formats error messages correctly."""
raw_error = "Error type: ollama_unavailable"
if "ollama_unavailable" in raw_error:
formatted_error = (
"Ollama AI service is unavailable. "
"Please check that Ollama is running properly on your system."
)
else:
formatted_error = raw_error
assert "Ollama AI service is unavailable" in formatted_error
def test_error_handler_stack_trace_inclusion(self):
"""Error handler can include stack trace."""
import traceback
try:
raise ValueError("Test error")
except ValueError:
stack_trace = traceback.format_exc()
assert "ValueError" in stack_trace
assert "Test error" in stack_trace
def test_error_handler_error_type_classification(self):
"""Error handler classifies error types correctly."""
error_messages = {
"status code: 503": "ollama_unavailable",
"status code: 404": "model_not_found",
"connection refused": "connection_error",
"rate limit exceeded": "rate_limit",
"unknown error": "unknown",
}
for msg, expected_type in error_messages.items():
if "503" in msg:
error_type = "ollama_unavailable"
elif "404" in msg:
error_type = "model_not_found"
elif "connection" in msg.lower():
error_type = "connection_error"
elif "rate limit" in msg.lower():
error_type = "rate_limit"
else:
error_type = "unknown"
assert error_type == expected_type
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_error_handler_socket_notification(self, mock_socket_class):
"""Error handler sends socket notifications."""
mock_socket = Mock()
mock_socket_class.return_value = mock_socket
# Simulate socket emit
mock_socket.emit_to_subscribers(
"research_progress",
123,
{"status": "failed", "error": "Test error"},
)
mock_socket.emit_to_subscribers.assert_called_once()
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_error_handler_cleanup_resources(self, mock_cleanup):
"""Error handler cleans up resources."""
active_research = {123: {"thread": Mock()}}
termination_flags = {}
# Simulate cleanup call
mock_cleanup(123, active_research, termination_flags, "testuser")
mock_cleanup.assert_called_once()
def test_error_handler_partial_results_preservation(self):
"""Error handler preserves partial results."""
partial_results = {
"findings": [{"content": "Finding 1", "phase": "search"}],
"iterations": 1,
}
# Partial results should be accessible
assert len(partial_results["findings"]) == 1
assert partial_results["iterations"] == 1
def test_error_handler_metadata_update(self):
"""Error handler updates metadata with error info."""
metadata = {"phase": "search", "progress": 50}
# Update metadata on error
metadata.update(
{
"phase": "error",
"error": "Research failed",
"error_type": "llm_error",
}
)
assert metadata["phase"] == "error"
assert "error" in metadata
assert "error_type" in metadata
def test_error_handler_concurrent_error_handling(self):
"""Error handler handles concurrent errors safely."""
import threading
errors_handled = []
lock = threading.Lock()
def handle_error(error_id):
with lock:
errors_handled.append(error_id)
# Simulate concurrent error handling
threads = []
for i in range(5):
t = threading.Thread(target=handle_error, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
assert len(errors_handled) == 5
@patch(
"local_deep_research.web.services.research_service.get_user_db_session"
)
def test_error_handler_database_error_during_error_handling(
self, mock_get_session
):
"""Error handler handles database errors during error handling."""
mock_session = MagicMock()
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=False)
mock_session.commit.side_effect = Exception("DB connection lost")
mock_get_session.return_value = mock_session
# Error during error handling should not crash
try:
mock_session.commit()
db_error = None
except Exception as e:
db_error = str(e)
assert db_error == "DB connection lost"
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_error_handler_socket_error_during_notification(
self, mock_socket_class
):
"""Error handler handles socket errors during notification."""
mock_socket = Mock()
mock_socket.emit_to_subscribers.side_effect = Exception(
"Socket disconnected"
)
mock_socket_class.return_value = mock_socket
# Socket error should not crash
try:
mock_socket.emit_to_subscribers("event", 123, {"data": "test"})
socket_error = None
except Exception as e:
socket_error = str(e)
assert socket_error == "Socket disconnected"
def test_error_handler_retry_after_recoverable_error(self):
"""Error handler identifies recoverable errors."""
error_message = "rate limit exceeded, please retry after 60 seconds"
is_recoverable = (
"rate limit" in error_message.lower()
or "retry" in error_message.lower()
or "timeout" in error_message.lower()
)
assert is_recoverable
class TestTerminationHandling:
"""Tests for termination handling."""
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_user_initiated_cancel(self, mock_cleanup, mock_queue):
"""Termination handles user-initiated cancel."""
from local_deep_research.web.services.research_service import (
handle_termination,
)
handle_termination(123, "testuser")
mock_queue.queue_error_update.assert_called_once()
call_kwargs = mock_queue.queue_error_update.call_args[1]
assert call_kwargs["status"] == "suspended"
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_during_analysis_phase(self, mock_cleanup, mock_queue):
"""Termination during analysis phase."""
termination_flags = {123: True}
# Check termination flag
if termination_flags.get(123):
terminated = True
else:
terminated = False
assert terminated
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_during_synthesis_phase(self, mock_cleanup, mock_queue):
"""Termination during synthesis phase."""
termination_flags = {123: True}
# Should check termination during synthesis
if termination_flags.get(123):
raise_termination = True
else:
raise_termination = False
assert raise_termination
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_during_report_generation(
self, mock_cleanup, mock_queue
):
"""Termination during report generation phase."""
termination_flags = {123: True}
if termination_flags.get(123):
should_stop = True
else:
should_stop = False
assert should_stop
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_cleanup_resources(self, mock_cleanup, mock_queue):
"""Termination cleans up resources."""
from local_deep_research.web.services.research_service import (
handle_termination,
)
handle_termination(123, "testuser")
mock_cleanup.assert_called_once()
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_database_status_update(self, mock_cleanup, mock_queue):
"""Termination updates database status."""
from local_deep_research.web.services.research_service import (
handle_termination,
)
handle_termination(123, "testuser")
# Should queue error update
mock_queue.queue_error_update.assert_called_once()
call_kwargs = mock_queue.queue_error_update.call_args[1]
assert call_kwargs["error_message"] == "Research was terminated by user"
@patch("local_deep_research.web.services.socket_service.SocketIOService")
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch(
"local_deep_research.web.services.research_service.cleanup_research_resources"
)
def test_termination_socket_notification(
self, mock_cleanup, mock_queue, mock_socket_class
):
"""Termination sends socket notification."""
mock_socket = Mock()
mock_socket_class.return_value = mock_socket
# Simulate socket emit during termination
mock_socket.emit_to_subscribers(
"research_progress",
123,
{"status": "suspended", "message": "Terminated by user"},
)
mock_socket.emit_to_subscribers.assert_called()
def test_termination_thread_interruption(self):
"""Termination handles thread interruption."""
import threading
thread_interrupted = threading.Event()
def research_thread():
# Simulate work
if thread_interrupted.is_set():
return
# Set interrupt flag
thread_interrupted.set()
# Thread should exit cleanly
assert thread_interrupted.is_set()
def test_termination_partial_results_preservation(self):
"""Termination preserves partial results."""
active_research = {
123: {
"thread": Mock(),
"progress": 50,
"status": "in_progress",
"partial_results": {"findings": ["partial"]},
}
}
# Partial results should be accessible before cleanup
partial = active_research[123].get("partial_results")
assert partial is not None
assert "findings" in partial
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
def test_termination_queue_processor_notification(self, mock_queue):
"""Termination notifies queue processor."""
# Simulate queue processor notification
mock_queue.queue_error_update(
username="testuser",
research_id=123,
status="suspended",
error_message="Research was terminated by user",
metadata={},
completed_at="2024-01-01T00:00:00Z",
report_path=None,
)
mock_queue.queue_error_update.assert_called_once()
class TestErrorContextPreservation:
"""Tests for error context preservation."""
def test_preserve_error_context_with_phase(self):
"""Error context preserves current phase."""
error_context = {
"phase": "synthesis",
"progress": 75,
"last_operation": "LLM invocation",
}
assert error_context["phase"] == "synthesis"
assert error_context["progress"] == 75
def test_preserve_error_context_with_iteration(self):
"""Error context preserves iteration count."""
error_context = {
"iteration": 3,
"total_iterations": 5,
}
assert error_context["iteration"] == 3
def test_preserve_error_context_with_search_results(self):
"""Error context preserves search results."""
error_context = {
"findings_count": 10,
"sources_collected": 25,
}
assert error_context["findings_count"] == 10
assert error_context["sources_collected"] == 25
class TestErrorRecovery:
"""Tests for error recovery strategies."""
def test_error_recovery_retry_with_backoff(self):
"""Error recovery uses exponential backoff."""
initial_delay = 1
max_retries = 3
delays = []
for attempt in range(max_retries):
delay = initial_delay * (2**attempt)
delays.append(delay)
assert delays == [1, 2, 4]
def test_error_recovery_fallback_to_simpler_mode(self):
"""Error recovery falls back to simpler mode."""
current_mode = "detailed"
error_occurred = True
if error_occurred and current_mode == "detailed":
fallback_mode = "quick"
else:
fallback_mode = current_mode
assert fallback_mode == "quick"
def test_error_recovery_skip_problematic_source(self):
"""Error recovery skips problematic sources."""
sources = ["good1", "bad", "good2", "bad2"]
problematic = ["bad", "bad2"]
filtered_sources = [s for s in sources if s not in problematic]
assert filtered_sources == ["good1", "good2"]