""" 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