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
539 lines
19 KiB
Python
539 lines
19 KiB
Python
"""
|
|
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
|