Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

181 lines
6.4 KiB
Python

"""
Test milestone logging functionality
"""
from datetime import datetime
from unittest.mock import MagicMock, Mock, patch
from local_deep_research.utilities.log_utils import (
_get_research_id,
frontend_progress_sink,
)
class TestMilestoneLogging:
"""Test milestone logging functionality"""
def test_get_research_id_from_bound_logger(self):
"""Test that _get_research_id extracts research_id from bound logger"""
# Create a mock record with bound research_id
record = {"extra": {"research_id": 123}}
research_id = _get_research_id(record)
assert research_id == 123
def test_get_research_id_from_flask_context(self):
"""Test that _get_research_id falls back to Flask g object"""
record = {}
with patch(
"local_deep_research.utilities.log_utils.has_app_context",
return_value=True,
):
mock_g = Mock()
mock_g.get.return_value = 456
with patch(
"local_deep_research.utilities.log_utils.g",
mock_g,
):
research_id = _get_research_id(record)
assert research_id == 456
def test_get_research_id_returns_none_when_not_found(self):
"""Test that _get_research_id returns None when no research_id is available"""
record = {}
with patch(
"local_deep_research.utilities.log_utils.has_app_context",
return_value=False,
):
research_id = _get_research_id(record)
assert research_id is None
@patch("local_deep_research.utilities.log_utils.SocketIOService")
def test_frontend_progress_sink_handles_milestone(
self, mock_socket_service
):
"""Test that frontend_progress_sink properly handles MILESTONE logs"""
# Mock the SocketIOService instance
mock_socket_instance = Mock()
mock_socket_service.return_value = mock_socket_instance
# Create a mock level object - Mock(name=...) sets the display name,
# not the .name attribute, so we must set it explicitly
level_mock = MagicMock()
level_mock.name = "MILESTONE"
time_mock = MagicMock()
time_mock.isoformat.return_value = "2023-01-01T00:00:00"
# Create a mock message with MILESTONE level
message = Mock()
message.record = {
"level": level_mock,
"message": "Generating search questions for iteration 1",
"time": time_mock,
"extra": {"research_id": 789},
}
# Call the sink
frontend_progress_sink(message)
# Verify emit_to_subscribers was called correctly
mock_socket_instance.emit_to_subscribers.assert_called_once_with(
"progress",
789,
{
"log_entry": {
"message": "Generating search questions for iteration 1",
"type": "MILESTONE",
"time": "2023-01-01T00:00:00",
}
},
enable_logging=False,
)
def test_milestone_logging_in_research_service(self):
"""Test that milestone logging uses logger.bind() in research service"""
# This test verifies the pattern used in research_service.py
# where logger.bind(research_id=research_id) is called before logging
from loguru import logger
# Mock the logger
with patch.object(logger, "bind") as mock_bind:
mock_bound_logger = Mock()
mock_bind.return_value = mock_bound_logger
# Simulate the pattern used in research_service.py
research_id = 999
message = "Test milestone message"
# This is how it's done in the actual code
bound_logger = logger.bind(research_id=research_id)
bound_logger.log("MILESTONE", message)
# Verify
mock_bind.assert_called_once_with(research_id=999)
mock_bound_logger.log.assert_called_once_with("MILESTONE", message)
def test_get_research_status_includes_milestone(self):
"""Test that get_research_status endpoint includes latest milestone.
The research_routes.get_research_status is a Flask route handler that
requires full request context. We verify the pattern works by testing
the milestone log entry structure directly.
"""
# Test that the expected log entry structure is correct
milestone_message = "Current milestone message"
log_entry = {
"message": milestone_message,
"type": "MILESTONE",
"time": datetime.utcnow().isoformat(),
}
assert log_entry["type"] == "MILESTONE"
assert log_entry["message"] == "Current milestone message"
assert "time" in log_entry
def test_milestone_logs_thread_safety(self):
"""Test that milestone logging works correctly across threads"""
from concurrent.futures import ThreadPoolExecutor
from loguru import logger
results = []
def thread_task(research_id):
# Simulate binding research_id and logging
with patch.object(logger, "bind") as mock_bind:
mock_bound_logger = Mock()
mock_bind.return_value = mock_bound_logger
# This is how it's done in the actual code
bound_logger = logger.bind(research_id=research_id)
bound_logger.log("MILESTONE", f"Thread {research_id} milestone")
# Store the call for verification
results.append(
{
"research_id": research_id,
"bind_call": mock_bind.call_args,
"log_call": mock_bound_logger.log.call_args,
}
)
# Run multiple threads
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(thread_task, i) for i in range(3)]
for future in futures:
future.result()
# Verify each thread had its own research_id binding (order-independent)
assert len(results) == 3
seen_ids = set()
for result in results:
rid = result["research_id"]
assert result["bind_call"][1]["research_id"] == rid
assert result["log_call"][0][1] == f"Thread {rid} milestone"
seen_ids.add(rid)
assert seen_ids == {0, 1, 2}