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

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