7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
514 lines
17 KiB
Python
514 lines
17 KiB
Python
"""
|
|
Tests for error_handling/error_reporter.py - Error Categorization
|
|
|
|
Tests cover:
|
|
- Error message pattern matching
|
|
- Category assignment for different error types
|
|
- Edge cases in pattern matching
|
|
- User-friendly error information
|
|
|
|
These tests ensure users get helpful error messages and guidance.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
class TestErrorCategorization:
|
|
"""Tests for error categorization logic."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_connection_error_detected(self, reporter):
|
|
"""'Connection refused' -> CONNECTION_ERROR."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Test various connection error patterns
|
|
test_cases = [
|
|
"Connection refused",
|
|
"POST predict EOF error",
|
|
"Connection failed",
|
|
"timeout waiting for response",
|
|
"HTTP error 500",
|
|
"network error occurred",
|
|
"[Errno 111] Connection refused",
|
|
"host.docker.internal not reachable",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.CONNECTION_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
def test_model_error_detected(self, reporter):
|
|
"""'Model not found' -> MODEL_ERROR."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
test_cases = [
|
|
"Model xyz not found",
|
|
"Invalid model specified",
|
|
"Ollama is not available",
|
|
"API key is invalid",
|
|
"Authentication error",
|
|
"max_workers must be greater than 0",
|
|
"TypeError Context Size",
|
|
"No auth credentials found",
|
|
"401 - API key",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.MODEL_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
def test_rate_limit_error_detected(self, reporter):
|
|
"""'rate limit' -> RATE_LIMIT_ERROR."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
test_cases = [
|
|
"429 resource exhausted",
|
|
"429 too many requests",
|
|
"rate limit exceeded",
|
|
"rate_limit hit",
|
|
"ratelimit reached",
|
|
"quota exceeded",
|
|
"resource exhausted - quota",
|
|
"LLM rate limit reached",
|
|
"API rate limit",
|
|
"maximum requests per minute",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.RATE_LIMIT_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
def test_timeout_error_detected(self, reporter):
|
|
"""'timeout' -> CONNECTION_ERROR (timeout is connection-related)."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Note: timeout is in CONNECTION_ERROR patterns
|
|
# The pattern matches "timeout" exactly (case-insensitive)
|
|
test_cases = [
|
|
"timeout",
|
|
"Connection timeout",
|
|
"The request timeout occurred", # Contains "timeout"
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
# Timeout is categorized as CONNECTION_ERROR
|
|
assert category == ErrorCategory.CONNECTION_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
def test_overlapping_patterns_priority(self, reporter):
|
|
"""First matching pattern wins."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Test message that could match multiple patterns
|
|
# "Connection timeout" has both "Connection" and "timeout"
|
|
category = reporter.categorize_error("Connection timeout")
|
|
|
|
# Should match CONNECTION_ERROR first
|
|
assert category == ErrorCategory.CONNECTION_ERROR
|
|
|
|
def test_partial_match_rejected(self, reporter):
|
|
"""'settimeout' doesn't match 'timeout' pattern."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# The regex pattern is just "timeout", which will match "settimeout"
|
|
# This tests the actual behavior
|
|
category = reporter.categorize_error("settimeout error occurred")
|
|
|
|
# Note: This WILL match because regex doesn't have word boundaries
|
|
# The pattern "timeout" is contained in "settimeout"
|
|
# This test documents the current behavior
|
|
assert category == ErrorCategory.CONNECTION_ERROR
|
|
|
|
def test_case_insensitive_matching(self, reporter):
|
|
"""'TIMEOUT' matches timeout pattern."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
test_cases = [
|
|
("TIMEOUT", ErrorCategory.CONNECTION_ERROR),
|
|
("RATE LIMIT", ErrorCategory.RATE_LIMIT_ERROR),
|
|
("MODEL NOT FOUND", ErrorCategory.MODEL_ERROR),
|
|
("CONNECTION REFUSED", ErrorCategory.CONNECTION_ERROR),
|
|
]
|
|
|
|
for error_msg, expected in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == expected, f"Failed for: {error_msg}"
|
|
|
|
def test_multiline_error_message(self, reporter):
|
|
"""Multi-line errors parsed correctly."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
multiline_error = """
|
|
Error occurred:
|
|
Connection refused
|
|
at line 123
|
|
in file xyz.py
|
|
"""
|
|
|
|
category = reporter.categorize_error(multiline_error)
|
|
assert category == ErrorCategory.CONNECTION_ERROR
|
|
|
|
def test_empty_error_returns_unknown(self, reporter):
|
|
"""Empty string -> UNKNOWN_ERROR."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
category = reporter.categorize_error("")
|
|
assert category == ErrorCategory.UNKNOWN_ERROR
|
|
|
|
category = reporter.categorize_error(" ")
|
|
assert category == ErrorCategory.UNKNOWN_ERROR
|
|
|
|
def test_very_long_error_performance(self, reporter):
|
|
"""10KB error message doesn't hang."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Create a very long error message (10KB)
|
|
long_error = "x" * 10000 + " Connection refused " + "y" * 10000
|
|
|
|
import time
|
|
|
|
start = time.time()
|
|
category = reporter.categorize_error(long_error)
|
|
elapsed = time.time() - start
|
|
|
|
# Should complete within reasonable time (< 1 second)
|
|
assert elapsed < 1.0
|
|
assert category == ErrorCategory.CONNECTION_ERROR
|
|
|
|
|
|
class TestUserFriendlyTitles:
|
|
"""Tests for user-friendly error titles."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_all_categories_have_titles(self, reporter):
|
|
"""All error categories have user-friendly titles."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
for category in ErrorCategory:
|
|
title = reporter.get_user_friendly_title(category)
|
|
assert title is not None
|
|
assert len(title) > 0
|
|
|
|
def test_title_content(self, reporter):
|
|
"""Titles are meaningful."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
expected_titles = {
|
|
ErrorCategory.CONNECTION_ERROR: "Connection Issue",
|
|
ErrorCategory.MODEL_ERROR: "LLM Service Error",
|
|
ErrorCategory.SEARCH_ERROR: "Search Service Error",
|
|
ErrorCategory.RATE_LIMIT_ERROR: "API Rate Limit Exceeded",
|
|
ErrorCategory.UNKNOWN_ERROR: "Unexpected Error",
|
|
}
|
|
|
|
for category, expected in expected_titles.items():
|
|
actual = reporter.get_user_friendly_title(category)
|
|
assert actual == expected
|
|
|
|
|
|
class TestSuggestedActions:
|
|
"""Tests for suggested action lists."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_all_categories_have_suggestions(self, reporter):
|
|
"""All categories have suggested actions."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
for category in ErrorCategory:
|
|
suggestions = reporter.get_suggested_actions(category)
|
|
assert suggestions is not None
|
|
assert isinstance(suggestions, list)
|
|
assert len(suggestions) > 0
|
|
|
|
def test_suggestions_are_actionable(self, reporter):
|
|
"""Suggestions contain actionable text."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
suggestions = reporter.get_suggested_actions(
|
|
ErrorCategory.CONNECTION_ERROR
|
|
)
|
|
|
|
# Should have multiple suggestions
|
|
assert len(suggestions) >= 2
|
|
|
|
# Each should be a non-empty string
|
|
for suggestion in suggestions:
|
|
assert isinstance(suggestion, str)
|
|
assert len(suggestion) > 10 # Meaningful text
|
|
|
|
|
|
class TestErrorAnalysis:
|
|
"""Tests for comprehensive error analysis."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_analyze_error_returns_complete_structure(self, reporter):
|
|
"""analyze_error returns all expected keys."""
|
|
analysis = reporter.analyze_error("Connection refused")
|
|
|
|
assert "category" in analysis
|
|
assert "title" in analysis
|
|
assert "original_error" in analysis
|
|
assert "suggestions" in analysis
|
|
assert "severity" in analysis
|
|
assert "recoverable" in analysis
|
|
|
|
def test_analyze_error_with_context(self, reporter):
|
|
"""Context information is included."""
|
|
context = {
|
|
"findings": [{"content": "some data"}],
|
|
"current_knowledge": "existing info",
|
|
}
|
|
|
|
analysis = reporter.analyze_error("Connection refused", context=context)
|
|
|
|
assert "context" in analysis
|
|
assert "has_partial_results" in analysis
|
|
assert analysis["has_partial_results"] is True
|
|
|
|
def test_severity_levels(self, reporter):
|
|
"""Severity levels are appropriate."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
severity_expectations = {
|
|
ErrorCategory.CONNECTION_ERROR: "high",
|
|
ErrorCategory.MODEL_ERROR: "high",
|
|
ErrorCategory.SEARCH_ERROR: "medium",
|
|
ErrorCategory.SYNTHESIS_ERROR: "low",
|
|
ErrorCategory.FILE_ERROR: "medium",
|
|
ErrorCategory.RATE_LIMIT_ERROR: "medium",
|
|
ErrorCategory.UNKNOWN_ERROR: "high",
|
|
}
|
|
|
|
for category, expected_severity in severity_expectations.items():
|
|
actual = reporter._determine_severity(category)
|
|
assert actual == expected_severity, f"Failed for {category}"
|
|
|
|
def test_recoverability(self, reporter):
|
|
"""Recoverability is correctly determined."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Most errors should be recoverable
|
|
recoverable_categories = [
|
|
ErrorCategory.CONNECTION_ERROR,
|
|
ErrorCategory.MODEL_ERROR,
|
|
ErrorCategory.SEARCH_ERROR,
|
|
ErrorCategory.SYNTHESIS_ERROR,
|
|
ErrorCategory.FILE_ERROR,
|
|
ErrorCategory.RATE_LIMIT_ERROR,
|
|
]
|
|
|
|
for category in recoverable_categories:
|
|
assert reporter._is_recoverable(category) is True
|
|
|
|
# Unknown errors are not recoverable
|
|
assert reporter._is_recoverable(ErrorCategory.UNKNOWN_ERROR) is False
|
|
|
|
|
|
class TestSearchErrorPatterns:
|
|
"""Tests for search-related error patterns."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_search_error_patterns(self, reporter):
|
|
"""Search error patterns are detected."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
test_cases = [
|
|
"Search failed",
|
|
"No search results found",
|
|
"Search engine error",
|
|
"The search is longer than 256 characters",
|
|
"Failed to create search engine",
|
|
"search engine 'brave' could not be found",
|
|
"GitHub API error",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.SEARCH_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
|
|
class TestSynthesisErrorPatterns:
|
|
"""Tests for synthesis-related error patterns."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_synthesis_error_patterns(self, reporter):
|
|
"""Synthesis error patterns are detected."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Note: "Synthesis timeout" would match CONNECTION_ERROR due to "timeout"
|
|
# Pattern matching is priority-based
|
|
test_cases = [
|
|
"Error during synthesis",
|
|
"Failed to generate report",
|
|
"detailed report stuck",
|
|
"report taking too long",
|
|
"progress at 100 stuck",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.SYNTHESIS_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
|
|
class TestFileErrorPatterns:
|
|
"""Tests for file-related error patterns."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_file_error_patterns(self, reporter):
|
|
"""File error patterns are detected."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorCategory,
|
|
)
|
|
|
|
# Note: "HTTP error 404" would match CONNECTION_ERROR first
|
|
test_cases = [
|
|
"Permission denied",
|
|
"File xyz not found",
|
|
"Cannot write to file",
|
|
"Disk is full",
|
|
"No module named local_deep_research",
|
|
"Attempt to write readonly database",
|
|
"database is locked",
|
|
"database table is locked",
|
|
]
|
|
|
|
for error_msg in test_cases:
|
|
category = reporter.categorize_error(error_msg)
|
|
assert category == ErrorCategory.FILE_ERROR, (
|
|
f"Failed for: {error_msg}"
|
|
)
|
|
|
|
|
|
class TestServiceNameExtraction:
|
|
"""Tests for service name extraction from errors."""
|
|
|
|
@pytest.fixture
|
|
def reporter(self):
|
|
"""Create an ErrorReporter instance."""
|
|
from local_deep_research.error_handling.error_reporter import (
|
|
ErrorReporter,
|
|
)
|
|
|
|
return ErrorReporter()
|
|
|
|
def test_extract_service_names(self, reporter):
|
|
"""Service names are extracted from error messages."""
|
|
test_cases = [
|
|
("OpenAI API error", "Openai"),
|
|
("Anthropic rate limit", "Anthropic"),
|
|
("Google API error", "Google"),
|
|
("Ollama connection failed", "Ollama"),
|
|
("SearXNG timeout", "Searxng"),
|
|
("Tavily search failed", "Tavily"),
|
|
("Brave search error", "Brave"),
|
|
("Unknown service error", "API Service"),
|
|
]
|
|
|
|
for error_msg, expected_service in test_cases:
|
|
actual = reporter._extract_service_name(error_msg)
|
|
assert actual == expected_service, f"Failed for: {error_msg}"
|