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

465 lines
15 KiB
Python

"""
Tests for research_service lifecycle management.
Tests cover:
- Resource cleanup
- Progress callbacks
"""
from unittest.mock import Mock, patch
import threading
class TestResourceCleanup:
"""Tests for resource cleanup."""
@patch("local_deep_research.settings.env_registry.is_test_mode")
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch("local_deep_research.web.routes.globals.cleanup_research")
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_cleanup_removes_from_active_dict(
self,
mock_socket,
mock_cleanup,
mock_queue,
mock_test_mode,
):
"""Cleanup calls cleanup_research to remove from active dicts."""
from local_deep_research.web.services.research_service import (
cleanup_research_resources,
)
mock_test_mode.return_value = False
cleanup_research_resources(123, "testuser")
mock_cleanup.assert_called_once_with(123)
@patch("local_deep_research.settings.env_registry.is_test_mode")
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch("local_deep_research.web.routes.globals.cleanup_research")
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_cleanup_notifies_queue_processor(
self,
mock_socket,
mock_cleanup,
mock_queue,
mock_test_mode,
):
"""Cleanup notifies queue processor of completion."""
from local_deep_research.web.services.research_service import (
cleanup_research_resources,
)
mock_test_mode.return_value = False
cleanup_research_resources(123, "testuser")
mock_queue.notify_research_completed.assert_called_once_with(
"testuser", 123, user_password=None
)
def test_cleanup_socket_emit_success(self):
"""Cleanup emits socket event on success."""
# Simulate socket emit
mock_socket = Mock()
# Emit to subscribers
mock_socket.emit_to_subscribers(
"research_complete",
123,
{"status": "completed"},
)
mock_socket.emit_to_subscribers.assert_called_once()
@patch("local_deep_research.settings.env_registry.is_test_mode")
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch("local_deep_research.web.routes.globals.cleanup_research")
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_cleanup_socket_emit_failure_handling(
self,
mock_socket_class,
mock_cleanup,
mock_queue,
mock_test_mode,
):
"""Cleanup handles socket emit failure gracefully."""
from local_deep_research.web.services.research_service import (
cleanup_research_resources,
)
mock_test_mode.return_value = False
mock_socket = Mock()
mock_socket.emit_to_subscribers.side_effect = Exception("Socket error")
mock_socket_class.return_value = mock_socket
# Should not raise even with socket error
cleanup_research_resources(123, "testuser")
@patch("local_deep_research.settings.env_registry.is_test_mode")
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
@patch("local_deep_research.web.routes.globals.cleanup_research")
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_cleanup_database_session_handling(
self,
mock_socket,
mock_cleanup,
mock_queue,
mock_test_mode,
):
"""Cleanup handles database session properly."""
from local_deep_research.web.services.research_service import (
cleanup_research_resources,
)
mock_test_mode.return_value = False
# Should complete without database errors
cleanup_research_resources(123, "testuser")
mock_queue.notify_research_completed.assert_called()
def test_cleanup_file_handle_closure(self):
"""Cleanup closes file handles."""
# Simulate file handle management
file_handles = []
class MockFileHandle:
def __init__(self):
self.closed = False
file_handles.append(self)
def close(self):
self.closed = True
# Create handles
MockFileHandle()
MockFileHandle()
# Cleanup closes handles
for handle in file_handles:
handle.close()
assert all(h.closed for h in file_handles)
def test_cleanup_memory_release(self):
"""Cleanup releases memory references."""
# Simulate memory management
large_data = {"data": "x" * 10000}
active_research = {123: {"results": large_data}}
# Cleanup removes references
del active_research[123]
assert 123 not in active_research
def test_cleanup_concurrent_access_safety(self):
"""Cleanup is thread-safe."""
active_research = {}
lock = threading.Lock()
cleanup_count = [0]
def cleanup(research_id):
with lock:
if research_id in active_research:
del active_research[research_id]
cleanup_count[0] += 1
# Add some research
for i in range(10):
active_research[i] = {"data": i}
# Concurrent cleanup
threads = []
for i in range(10):
t = threading.Thread(target=cleanup, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
assert len(active_research) == 0
assert cleanup_count[0] == 10
class TestProgressCallbacks:
"""Tests for progress callback functionality."""
def test_progress_callback_sequencing(self):
"""Progress callbacks are called in sequence."""
calls = []
def progress_callback(message, progress, metadata):
calls.append((message, progress, metadata.get("phase")))
# Simulate progress sequence
progress_callback("Starting", 0, {"phase": "init"})
progress_callback("Searching", 20, {"phase": "search"})
progress_callback("Analyzing", 50, {"phase": "analysis"})
progress_callback("Synthesizing", 80, {"phase": "synthesis"})
progress_callback("Complete", 100, {"phase": "complete"})
assert len(calls) == 5
assert calls[0][1] == 0
assert calls[-1][1] == 100
assert calls[-1][2] == "complete"
def test_progress_callback_percentage_accuracy(self):
"""Progress callbacks report accurate percentages."""
progress_values = []
def progress_callback(message, progress, metadata):
if progress is not None:
progress_values.append(progress)
# Simulate progress
for p in [0, 10, 25, 50, 75, 90, 100]:
progress_callback("Progress", p, {})
# All values should be in valid range
assert all(0 <= p <= 100 for p in progress_values)
# Values should be non-decreasing (progress doesn't go backwards)
for i in range(1, len(progress_values)):
assert progress_values[i] >= progress_values[i - 1]
def test_progress_callback_status_message_updates(self):
"""Progress callbacks include meaningful status messages."""
messages = []
def progress_callback(message, progress, metadata):
messages.append(message)
progress_callback("Starting research process", 5, {"phase": "init"})
progress_callback("Searching for information", 30, {"phase": "search"})
progress_callback(
"Generating summary", 90, {"phase": "output_generation"}
)
assert "Starting" in messages[0]
assert "Searching" in messages[1]
assert "summary" in messages[2].lower()
@patch("local_deep_research.web.services.socket_service.SocketIOService")
def test_progress_callback_socket_integration(self, mock_socket_class):
"""Progress callbacks integrate with socket service."""
mock_socket = Mock()
mock_socket_class.return_value = mock_socket
def progress_callback(research_id, progress, metadata):
mock_socket.emit_to_subscribers(
"progress",
research_id,
{"progress": progress, **metadata},
)
progress_callback(123, 50, {"phase": "search"})
mock_socket.emit_to_subscribers.assert_called_once_with(
"progress",
123,
{"progress": 50, "phase": "search"},
)
@patch("local_deep_research.web.queue.processor_v2.queue_processor")
def test_progress_callback_database_update(self, mock_queue):
"""Progress callbacks queue database updates."""
def progress_callback(username, research_id, progress):
mock_queue.queue_progress_update(username, research_id, progress)
progress_callback("testuser", 123, 75)
mock_queue.queue_progress_update.assert_called_once_with(
"testuser", 123, 75
)
def test_progress_callback_rate_limiting(self):
"""Progress callbacks respect rate limiting."""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: DELETE).
import time
last_call_time = [0]
min_interval = 0.1 # 100ms
calls_made = []
def rate_limited_callback(progress):
current_time = time.time()
if current_time - last_call_time[0] >= min_interval:
calls_made.append(progress)
last_call_time[0] = current_time
# Rapid calls
for i in range(10):
rate_limited_callback(i * 10)
time.sleep(0.05) # 50ms between calls
# Only about half should go through due to rate limiting
assert len(calls_made) < 10
def test_progress_callback_error_handling(self):
"""Progress callbacks handle errors gracefully."""
errors = []
def progress_callback(progress, metadata):
try:
if metadata.get("fail"):
raise ValueError("Callback error")
except Exception as e:
errors.append(str(e))
# Normal call
progress_callback(50, {})
# Error call
progress_callback(60, {"fail": True})
assert len(errors) == 1
assert "Callback error" in errors[0]
class TestResearchPhaseTracking:
"""Tests for research phase tracking."""
def test_phase_transition_init_to_search(self):
"""Phase transitions from init to search."""
phases = []
def track_phase(phase):
phases.append(phase)
track_phase("init")
track_phase("search")
assert phases == ["init", "search"]
def test_phase_transition_search_to_analysis(self):
"""Phase transitions from search to analysis."""
current_phase = "search"
next_phase = "analysis"
assert current_phase != next_phase
def test_phase_transition_analysis_to_synthesis(self):
"""Phase transitions from analysis to synthesis."""
phases = ["init", "search", "analysis", "synthesis"]
# Synthesis follows analysis
analysis_idx = phases.index("analysis")
synthesis_idx = phases.index("synthesis")
assert synthesis_idx == analysis_idx + 1
def test_phase_transition_synthesis_to_complete(self):
"""Phase transitions from synthesis to complete."""
all_phases = [
"init",
"search",
"analysis",
"synthesis",
"output_generation",
"complete",
]
# Complete is the final phase
assert all_phases[-1] == "complete"
def test_phase_metadata_preservation(self):
"""Phase metadata is preserved across transitions."""
phase_metadata = {}
def update_phase(phase, **kwargs):
phase_metadata[phase] = kwargs
update_phase("search", engine="google", results_count=10)
update_phase("analysis", findings_count=5)
update_phase("synthesis", tokens_used=1000)
assert phase_metadata["search"]["results_count"] == 10
assert phase_metadata["analysis"]["findings_count"] == 5
assert phase_metadata["synthesis"]["tokens_used"] == 1000
class TestActiveResearchManagement:
"""Tests for active research dictionary management."""
def test_active_research_creation(self):
"""Active research entry is created correctly."""
active_research = {}
active_research[123] = {
"thread": Mock(),
"progress": 0,
"status": "in_progress",
"log": [],
"settings": {"model": "gpt-4"},
}
assert 123 in active_research
assert active_research[123]["status"] == "in_progress"
def test_active_research_progress_update(self):
"""Active research progress is updated correctly."""
active_research = {123: {"progress": 0, "status": "in_progress"}}
active_research[123]["progress"] = 50
assert active_research[123]["progress"] == 50
def test_active_research_status_transition(self):
"""Active research status transitions correctly."""
active_research = {123: {"status": "in_progress"}}
# Transition to completed
active_research[123]["status"] = "completed"
assert active_research[123]["status"] == "completed"
def test_active_research_removal(self):
"""Active research entry is removed on cleanup."""
active_research = {123: {"status": "completed"}}
del active_research[123]
assert 123 not in active_research
def test_active_research_thread_safety(self):
"""Active research is thread-safe with locking."""
active_research = {}
lock = threading.Lock()
def add_research(research_id):
with lock:
active_research[research_id] = {"status": "in_progress"}
def remove_research(research_id):
with lock:
if research_id in active_research:
del active_research[research_id]
# Add and remove concurrently
threads = []
for i in range(10):
t1 = threading.Thread(target=add_research, args=(i,))
threads.append(t1)
t1.start()
for t in threads:
t.join()
# All should be added
assert len(active_research) == 10
# Remove all
threads = []
for i in range(10):
t = threading.Thread(target=remove_research, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
assert len(active_research) == 0