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

340 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Coverage tests for log_utils module.
Targets branches not exercised by test_log_utils.py and
test_log_utils_extended.py:
- InterceptHandler.emit: depth-walking loop stops at non-logging frame
- _get_research_id: None when no app context and no record extra
- _get_research_id: reads from record["extra"]["research_id"]
- _process_log_queue: stop-event exits the loop; None entry is skipped
- database_sink: username extraction from record["extra"]["username"]
- flush_log_queue: handles exception inside loop gracefully
- config_logger: debug=True path warns about unsafe logging
- config_logger: file logging disabled by default (no 4th add call)
"""
_logging = __import__("logging") # test needs stdlib LogRecord
from datetime import datetime # noqa: E402
from unittest.mock import Mock, patch # noqa: E402
MODULE = "local_deep_research.utilities.log_utils"
# ---------------------------------------------------------------------------
# InterceptHandler
# ---------------------------------------------------------------------------
class TestInterceptHandlerDepthWalking:
"""The depth-walking loop in emit should traverse past logging frames."""
def test_emit_with_valid_level(self):
from local_deep_research.utilities.log_utils import InterceptHandler
handler = InterceptHandler()
record = _logging.LogRecord(
name="test",
level=_logging.WARNING,
pathname="test.py",
lineno=10,
msg="warn",
args=(),
exc_info=None,
)
with patch(f"{MODULE}.logger") as mock_logger:
mock_level = Mock()
mock_level.name = "WARNING"
mock_logger.level.return_value = mock_level
mock_opt = Mock()
mock_logger.opt.return_value = mock_opt
handler.emit(record)
mock_opt.log.assert_called_once()
# level argument should be the resolved name
args = mock_opt.log.call_args[0]
assert args[0] == "WARNING"
def test_emit_with_unknown_level_uses_levelno(self):
from local_deep_research.utilities.log_utils import InterceptHandler
handler = InterceptHandler()
record = _logging.LogRecord(
name="test",
level=37,
pathname="test.py",
lineno=1,
msg="custom",
args=(),
exc_info=None,
)
record.levelname = "CUSTOM_LEVEL"
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level.side_effect = ValueError("unknown")
mock_opt = Mock()
mock_logger.opt.return_value = mock_opt
handler.emit(record)
# levelno (37) used when name lookup fails
args = mock_opt.log.call_args[0]
assert args[0] == 37
# ---------------------------------------------------------------------------
# _get_research_id
# ---------------------------------------------------------------------------
class TestGetResearchId:
def test_returns_none_when_no_context_and_no_extra(self):
from local_deep_research.utilities.log_utils import _get_research_id
with patch(f"{MODULE}.has_app_context", return_value=False):
result = _get_research_id(record={"extra": {}})
assert result is None
def test_returns_id_from_record_extra(self):
from local_deep_research.utilities.log_utils import _get_research_id
record = {"extra": {"research_id": "record-123"}}
result = _get_research_id(record=record)
assert result == "record-123"
def test_returns_id_from_flask_g_when_no_extra(self):
from local_deep_research.utilities.log_utils import _get_research_id
mock_g = Mock()
mock_g.get.return_value = "flask-456"
with patch(f"{MODULE}.has_app_context", return_value=True):
with patch(f"{MODULE}.g", mock_g):
result = _get_research_id(record={"extra": {}})
assert result == "flask-456"
def test_returns_none_with_no_record(self):
from local_deep_research.utilities.log_utils import _get_research_id
with patch(f"{MODULE}.has_app_context", return_value=False):
result = _get_research_id(record=None)
assert result is None
# ---------------------------------------------------------------------------
# database_sink username extraction
# ---------------------------------------------------------------------------
class TestDatabaseSinkUsernameExtraction:
def _make_message(self, extra=None):
msg = Mock()
msg.record = {
"time": datetime.now(),
"message": "test",
"name": "mod",
"function": "fn",
"line": 1,
"level": Mock(name="INFO"),
"extra": extra or {},
}
return msg
def test_username_from_extra_is_queued(self):
from local_deep_research.utilities.log_utils import database_sink
import local_deep_research.utilities.log_utils as mod
msg = self._make_message(extra={"username": "alice"})
with patch.object(mod, "has_app_context", return_value=False):
with patch.object(mod, "_log_queue") as mock_q:
database_sink(msg)
queued = mock_q.put_nowait.call_args[0][0]
assert queued["username"] == "alice"
def test_no_username_queues_none(self):
"""When research_id is set but username isn't, queue entry has
username=None — the daemon's get_user_db_session will then look it
up via session/thread-context fallbacks."""
from local_deep_research.utilities.log_utils import database_sink
import local_deep_research.utilities.log_utils as mod
msg = self._make_message(extra={"research_id": "rid-1"})
with patch.object(mod, "has_app_context", return_value=False):
with patch.object(mod, "_log_queue") as mock_q:
database_sink(msg)
queued = mock_q.put_nowait.call_args[0][0]
assert queued["username"] is None
def test_no_research_context_does_not_queue(self):
"""ResearchLog is research-scoped — logs with neither research_id
nor username should be skipped, not queued."""
from local_deep_research.utilities.log_utils import database_sink
import local_deep_research.utilities.log_utils as mod
msg = self._make_message(extra={})
with patch.object(mod, "has_app_context", return_value=False):
with patch.object(mod, "_log_queue") as mock_q:
database_sink(msg)
mock_q.put_nowait.assert_not_called()
# ---------------------------------------------------------------------------
# flush_log_queue exception inside loop
# ---------------------------------------------------------------------------
class TestFlushLogQueueExceptionHandling:
def test_exception_in_write_does_not_abort_flush(self):
"""If _write_log_to_database raises, flush should continue."""
from local_deep_research.utilities.log_utils import flush_log_queue
import local_deep_research.utilities.log_utils as mod
entry = {
"timestamp": datetime.now(),
"message": "m",
"module": "m",
"function": "f",
"line_no": 1,
"level": "INFO",
"research_id": None,
"username": None,
}
with patch.object(mod, "_log_queue") as mock_q:
mock_q.empty.side_effect = [False, True]
mock_q.get_nowait.return_value = entry
with patch.object(
mod, "_write_log_to_database", side_effect=Exception("db fail")
):
# Should not raise
flush_log_queue()
# ---------------------------------------------------------------------------
# config_logger debug path and no-file-logging path
# ---------------------------------------------------------------------------
class TestConfigLoggerDebugAndFileLogging:
def test_debug_true_adds_warning(self):
from local_deep_research.utilities.log_utils import config_logger
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app", debug=True)
mock_logger.warning.assert_called()
warning_msg = mock_logger.warning.call_args[0][0]
assert "DEBUG" in warning_msg or "debug" in warning_msg.lower()
def test_file_logging_disabled_by_default(self):
"""With LDR_ENABLE_FILE_LOGGING unset, no file sink is added."""
from local_deep_research.utilities.log_utils import config_logger
import os
env = {
k: v
for k, v in os.environ.items()
if k != "LDR_ENABLE_FILE_LOGGING"
}
with patch.dict("os.environ", env, clear=True):
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app")
# Only 3 sinks: stderr, database_sink, frontend_progress_sink
assert mock_logger.add.call_count == 3
def test_stderr_level_info_when_not_debug(self):
from local_deep_research.utilities.log_utils import config_logger
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app", debug=False)
# First add call is stderr
first_add = mock_logger.add.call_args_list[0]
assert first_add[1].get("level") == "INFO"
def test_stderr_level_debug_when_debug_true(self):
from local_deep_research.utilities.log_utils import config_logger
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app", debug=True)
first_add = mock_logger.add.call_args_list[0]
assert first_add[1].get("level") == "DEBUG"
# ---------------------------------------------------------------------------
# Sink-level diagnose policy (#4182): even when the operator opts into
# LDR_LOGURU_DIAGNOSE, the persisted DB sink and the browser-facing
# frontend sink must NEVER render frame locals (which can hold the
# SQLCipher master password). Only the local stderr sink may.
# ---------------------------------------------------------------------------
class TestDiagnoseSinkPolicy:
def test_diagnose_never_enabled_on_db_or_frontend_sink(self):
from local_deep_research.utilities.log_utils import (
config_logger,
database_sink,
frontend_progress_sink,
)
import os
env = {
k: v
for k, v in os.environ.items()
if k != "LDR_ENABLE_FILE_LOGGING"
}
# Opt into diagnose the way an operator debugging locally would.
env["LDR_LOGURU_DIAGNOSE"] = "true"
with patch.dict("os.environ", env, clear=True):
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app", debug=True)
# Map each add() call to its sink (first positional arg).
by_sink = {
call.args[0]: call.kwargs.get("diagnose")
for call in mock_logger.add.call_args_list
}
# stderr opted in -> diagnose may be True; the persisted and
# shipped sinks must be hard False regardless.
assert by_sink[database_sink] is False
assert by_sink[frontend_progress_sink] is False
def test_stderr_gets_diagnose_when_opted_in(self):
import sys
from local_deep_research.utilities.log_utils import config_logger
import os
env = {
k: v
for k, v in os.environ.items()
if k != "LDR_ENABLE_FILE_LOGGING"
}
env["LDR_LOGURU_DIAGNOSE"] = "true"
with patch.dict("os.environ", env, clear=True):
with patch(f"{MODULE}.logger") as mock_logger:
config_logger("app", debug=True)
stderr_calls = [
c
for c in mock_logger.add.call_args_list
if c.args and c.args[0] is sys.stderr
]
assert stderr_calls, "stderr sink was not configured"
assert stderr_calls[0].kwargs.get("diagnose") is True