chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
"""
|
||||
Coverage tests for local_deep_research/web/app.py.
|
||||
|
||||
Targets the ~50 missing statements in the main() function by patching
|
||||
heavy dependencies and exercising each branch individually.
|
||||
|
||||
All lazy imports inside main() (start_connection_cleanup_scheduler,
|
||||
session_manager, db_manager) are patched at their canonical module paths
|
||||
because they are not present as attributes on the app module itself.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
MODULE = "local_deep_research.web.app"
|
||||
|
||||
# Canonical patch targets for lazy imports inside main()
|
||||
_CLEANUP_MOD = "local_deep_research.web.auth.connection_cleanup"
|
||||
_SESSION_MOD = "local_deep_research.web.auth.session_manager"
|
||||
_DB_MOD = "local_deep_research.database.encrypted_db"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_log_queue_daemon():
|
||||
"""Prevent main() from spawning a real log-queue daemon thread during tests."""
|
||||
with (
|
||||
patch(f"{MODULE}.start_log_queue_processor"),
|
||||
patch(f"{MODULE}.stop_log_queue_processor"),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _handler_by_name(captured, name):
|
||||
"""Locate an atexit handler by function name (order-independent)."""
|
||||
for fn in captured:
|
||||
if getattr(fn, "__name__", None) == name:
|
||||
return fn
|
||||
raise AssertionError(
|
||||
f"No captured handler named {name!r}; got "
|
||||
f"{[getattr(fn, '__name__', repr(fn)) for fn in captured]}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_config(
|
||||
host="127.0.0.1",
|
||||
port=5000,
|
||||
debug=False,
|
||||
use_https=False,
|
||||
):
|
||||
return {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"debug": debug,
|
||||
"use_https": use_https,
|
||||
}
|
||||
|
||||
|
||||
def _patch_main(config, env=None, cleanup_side_effect=None):
|
||||
"""Return a context-manager stack that patches everything main() needs.
|
||||
|
||||
Usage::
|
||||
|
||||
with _patch_main(config) as mocks:
|
||||
from local_deep_research.web import app as app_module
|
||||
app_module.main()
|
||||
mocks["socket_service"].run.assert_called_once()
|
||||
"""
|
||||
mock_socket_service = MagicMock()
|
||||
mock_app = MagicMock()
|
||||
mock_scheduler = MagicMock()
|
||||
|
||||
patches = [
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(mock_app, mock_socket_service)
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
side_effect=cleanup_side_effect,
|
||||
return_value=mock_scheduler,
|
||||
),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
]
|
||||
|
||||
env_patch = patch.dict("os.environ", env or {}, clear=False)
|
||||
|
||||
class _Stack:
|
||||
def __enter__(self_inner):
|
||||
env_patch.start()
|
||||
for p in patches:
|
||||
p.start()
|
||||
self_inner.mocks = {
|
||||
"socket_service": mock_socket_service,
|
||||
"app": mock_app,
|
||||
"scheduler": mock_scheduler,
|
||||
}
|
||||
return self_inner.mocks
|
||||
|
||||
def __exit__(self_inner, *_):
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
env_patch.stop()
|
||||
|
||||
return _Stack()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: logging configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMainLogging:
|
||||
"""main() configures logging with the correct debug flag."""
|
||||
|
||||
def test_config_logger_called_with_debug_true(self):
|
||||
config = _make_config(debug=True)
|
||||
with _patch_main(config, env={"WERKZEUG_RUN_MAIN": "true"}):
|
||||
with patch(f"{MODULE}.config_logger") as mock_cl:
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_cl.assert_called_once_with("ldr_web", debug=True)
|
||||
|
||||
def test_config_logger_called_with_debug_false(self):
|
||||
config = _make_config(debug=False)
|
||||
with _patch_main(config):
|
||||
with patch(f"{MODULE}.config_logger") as mock_cl:
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_cl.assert_called_once_with("ldr_web", debug=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: HTTPS branch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMainHttpsBranch:
|
||||
"""main() logs a warning when use_https=True."""
|
||||
|
||||
def test_https_branch_does_not_raise(self):
|
||||
"""use_https=True must not raise; it only emits log warnings."""
|
||||
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
|
||||
config = _make_config(use_https=True)
|
||||
with _patch_main(config):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main() # must complete without exception
|
||||
|
||||
def test_no_https_branch_runs_cleanly(self):
|
||||
"""use_https=False takes the happy path without any HTTPS logging."""
|
||||
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
|
||||
config = _make_config(use_https=False)
|
||||
with _patch_main(config):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: cleanup-scheduler startup branch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMainCleanupSchedulerBranch:
|
||||
"""Guards for the conditional scheduler startup inside main()."""
|
||||
|
||||
def test_scheduler_started_when_not_debug(self):
|
||||
"""debug=False always starts the cleanup scheduler."""
|
||||
config = _make_config(debug=False)
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
) as mock_start,
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_start.assert_called_once()
|
||||
|
||||
def test_scheduler_started_when_werkzeug_run_main_set(self):
|
||||
"""debug=True with WERKZEUG_RUN_MAIN=true still starts the scheduler."""
|
||||
config = _make_config(debug=True)
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
) as mock_start,
|
||||
patch.dict(
|
||||
"os.environ", {"WERKZEUG_RUN_MAIN": "true"}, clear=False
|
||||
),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_start.assert_called_once()
|
||||
|
||||
def test_scheduler_not_started_in_debug_without_werkzeug(self):
|
||||
"""debug=True without WERKZEUG_RUN_MAIN skips scheduler startup."""
|
||||
config = _make_config(debug=True)
|
||||
# Ensure WERKZEUG_RUN_MAIN is absent for this test
|
||||
env_without = {
|
||||
k: v for k, v in os.environ.items() if k != "WERKZEUG_RUN_MAIN"
|
||||
}
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
) as mock_start,
|
||||
patch.dict("os.environ", env_without, clear=True),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_start.assert_not_called()
|
||||
|
||||
def test_scheduler_failure_does_not_abort_startup(self):
|
||||
"""If start_connection_cleanup_scheduler raises, main() still runs the server."""
|
||||
config = _make_config(debug=False)
|
||||
mock_socket_service = MagicMock()
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app",
|
||||
return_value=(MagicMock(), mock_socket_service),
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
side_effect=RuntimeError("scheduler boom"),
|
||||
),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main() # must not raise
|
||||
mock_socket_service.run.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: atexit registrations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMainAtexitRegistrations:
|
||||
"""main() must register at least 3 atexit handlers."""
|
||||
|
||||
def test_multiple_atexit_handlers_registered(self):
|
||||
config = _make_config()
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register") as mock_reg,
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
# Expect: shutdown_databases, shutdown_scheduler, cleanup-scheduler
|
||||
# lambda, flush_logs_on_exit, stop_log_queue_processor
|
||||
assert mock_reg.call_count >= 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: socket_service.run() call
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMainSocketServiceRun:
|
||||
"""main() forwards host/port/debug to socket_service.run()."""
|
||||
|
||||
def test_run_called_with_host_port_debug(self):
|
||||
config = _make_config(host="0.0.0.0", port=8080, debug=False)
|
||||
mock_socket_service = MagicMock()
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app",
|
||||
return_value=(MagicMock(), mock_socket_service),
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_socket_service.run.assert_called_once_with(
|
||||
host="0.0.0.0", port=8080, debug=False
|
||||
)
|
||||
|
||||
def test_run_called_with_debug_true(self):
|
||||
config = _make_config(host="localhost", port=5001, debug=True)
|
||||
mock_socket_service = MagicMock()
|
||||
with (
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app",
|
||||
return_value=(MagicMock(), mock_socket_service),
|
||||
),
|
||||
patch("atexit.register"),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch.dict(
|
||||
"os.environ", {"WERKZEUG_RUN_MAIN": "true"}, clear=False
|
||||
),
|
||||
):
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
mock_socket_service.run.assert_called_once_with(
|
||||
host="localhost", port=5001, debug=True
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: flush_logs_on_exit closure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFlushLogsOnExit:
|
||||
"""The first atexit handler (flush_logs_on_exit) must flush the log queue."""
|
||||
|
||||
def _run_and_capture(self, extra_patches=None):
|
||||
captured = []
|
||||
config = _make_config()
|
||||
extra_patches = extra_patches or []
|
||||
|
||||
patches = [
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register", side_effect=captured.append),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
] + extra_patches
|
||||
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
return captured
|
||||
|
||||
def test_flush_handler_calls_flush_log_queue(self):
|
||||
with patch(f"{MODULE}.flush_log_queue") as mock_flush:
|
||||
captured = self._run_and_capture()
|
||||
_handler_by_name(captured, "flush_logs_on_exit")()
|
||||
mock_flush.assert_called_once()
|
||||
|
||||
def test_flush_handler_swallows_exceptions(self):
|
||||
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
|
||||
with patch(
|
||||
f"{MODULE}.flush_log_queue", side_effect=RuntimeError("log error")
|
||||
):
|
||||
captured = self._run_and_capture()
|
||||
_handler_by_name(captured, "flush_logs_on_exit")() # must not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: shutdown_scheduler closure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestShutdownSchedulerHandler:
|
||||
"""The shutdown_scheduler atexit handler."""
|
||||
|
||||
def _run_and_capture_handlers(self, mock_app):
|
||||
captured = []
|
||||
config = _make_config()
|
||||
|
||||
patches = [
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(f"{MODULE}.create_app", return_value=(mock_app, MagicMock())),
|
||||
patch("atexit.register", side_effect=captured.append),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
]
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
return captured
|
||||
|
||||
def test_shutdown_scheduler_stops_news_scheduler(self):
|
||||
mock_app = MagicMock()
|
||||
mock_background_job_scheduler = MagicMock()
|
||||
mock_app.background_job_scheduler = mock_background_job_scheduler
|
||||
|
||||
captured = self._run_and_capture_handlers(mock_app)
|
||||
for handler in captured:
|
||||
try:
|
||||
handler()
|
||||
except Exception:
|
||||
pass
|
||||
mock_background_job_scheduler.stop.assert_called_once()
|
||||
|
||||
def test_shutdown_scheduler_noop_without_news_scheduler(self):
|
||||
"""app with no news_scheduler attribute does not raise."""
|
||||
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
|
||||
mock_app = MagicMock(spec=[]) # no attributes at all
|
||||
|
||||
captured = self._run_and_capture_handlers(mock_app)
|
||||
for handler in captured:
|
||||
try:
|
||||
handler()
|
||||
except Exception:
|
||||
pass # other handlers (e.g. shutdown_databases) may raise; that is fine
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: shutdown_databases closure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestShutdownDatabasesHandler:
|
||||
"""The shutdown_databases atexit handler (last registered)."""
|
||||
|
||||
def _run_and_capture_handlers(self):
|
||||
captured = []
|
||||
config = _make_config()
|
||||
|
||||
patches = [
|
||||
patch(f"{MODULE}.load_server_config", return_value=config),
|
||||
patch(f"{MODULE}.config_logger"),
|
||||
patch(
|
||||
f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
|
||||
),
|
||||
patch("atexit.register", side_effect=captured.append),
|
||||
patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
|
||||
patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
|
||||
patch(
|
||||
f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
]
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
from local_deep_research.web import app as app_module
|
||||
|
||||
app_module.main()
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
return captured
|
||||
|
||||
def test_shutdown_databases_calls_close_all_databases(self):
|
||||
mock_db_manager = MagicMock()
|
||||
captured = self._run_and_capture_handlers()
|
||||
handler = _handler_by_name(captured, "shutdown_databases")
|
||||
with patch(f"{_DB_MOD}.db_manager", mock_db_manager):
|
||||
handler()
|
||||
mock_db_manager.close_all_databases.assert_called_once()
|
||||
|
||||
def test_shutdown_databases_swallows_exceptions(self):
|
||||
# audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
|
||||
captured = self._run_and_capture_handlers()
|
||||
handler = _handler_by_name(captured, "shutdown_databases")
|
||||
with patch(f"{_DB_MOD}.db_manager", side_effect=ImportError("db gone")):
|
||||
handler() # must not propagate
|
||||
Reference in New Issue
Block a user