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

159 lines
6.9 KiB
Python

# allow: no-sut-import — guardian; runs create_app()/imports the SUT startup
# path in a fresh subprocess (in-process imports would mask the regression)
# and asserts the sentence-transformers / langchain-text-splitters stack is
# absent from sys.modules.
"""Regression guard for the app-startup import weight.
Background
----------
``create_app()`` runs on every app startup. It used to load the
sentence-transformers / langchain-text-splitters stack (~2s+, and on the
scheduler path the full transformers/torch stack, ~6-10s) at import time,
which on slow CI runners pushed startup past the 20s ``faulthandler``
watchdog (``responsive-test-gate / ui-tests``) and hung the app. The
original faulthandler dump showed the main thread stuck importing
``scheduler/background.py`` (it transitively imported the RAG service ->
``embeddings.splitters`` -> ``langchain_text_splitters`` -> sentence
transformers). The lazy text-splitter fix landed in #4829; see also the
earlier #4490 / #4431.
What this guard does (and does NOT) assert
------------------------------------------
#4829 removed the **sentence-transformers / langchain-text-splitters**
stack from the startup path (it is now imported lazily, only when text
splitting / RAG indexing actually runs). That is the invariant guarded
here.
It does NOT assert ``transformers`` / ``torch`` are absent from
``create_app()``. Those are STILL imported at startup through a separate,
pre-existing path not addressed by the lazy-splitter fix:
``web/app_factory.py`` register_blueprints -> most route blueprints ->
``search_system`` / ``config.llm_config`` ->
``langchain_core.language_models``, whose ``base`` module imports
``transformers`` at module load (``from transformers import
GPT2TokenizerFast`` — inside a ``try/except ImportError``, but transformers
is installed here, so it fires). ``BaseChatModel`` is imported at module
level by ~15 core modules and most startup route blueprints, so removing
transformers/torch from ``create_app()`` is an architectural change well
beyond the splitter fix. Asserting their absence here would be a guaranteed
false failure.
Both checks run in a FRESH subprocess: the broader pytest session has
almost always already imported these modules into ``sys.modules``, which
would mask any regression in-process.
If a check fails, some module on the startup path grew a module-level
import that drags in the splitter stack. Fix it by deferring that import
to the function where it is used (an RAG / text-splitting job), not by
relaxing this test.
"""
import subprocess
import sys
import pytest
# Probe 1: run the REAL create_app() in a clean interpreter and assert the
# splitter stack never loads. Covers the ENTIRE startup surface (every
# blueprint + middleware + queue processor), so there is no "which module did
# we forget to import" blind spot. Side-effecting singletons are patched out
# and all state is redirected to a throwaway data dir for determinism.
_PROBE_CREATE_APP = """
import sys, os, shutil, tempfile
_data_dir = tempfile.mkdtemp(prefix="ldr_startup_guard_")
os.environ["LDR_DATA_DIR"] = _data_dir
os.environ.setdefault("TESTING", "1")
from unittest.mock import patch, MagicMock
try:
with (
patch("local_deep_research.web.app_factory.SocketIOService"),
patch("local_deep_research.web.queue.processor_v2.queue_processor"),
patch("atexit.register"),
patch(
"local_deep_research.scheduler.background.get_background_job_scheduler",
return_value=MagicMock(),
),
):
from local_deep_research.web.app_factory import create_app
create_app()
stack = ("sentence_transformers", "langchain_text_splitters")
loaded = sorted(m for m in stack if m in sys.modules)
if loaded:
print("SPLITTER_STACK_IMPORTED:" + ",".join(loaded))
sys.exit(1)
print("CREATE_APP_SPLITTER_CLEAN")
finally:
# create_app() writes a .secret_key into LDR_DATA_DIR; clean it up so the
# guard doesn't leak a temp dir per run on persistent dev machines.
shutil.rmtree(_data_dir, ignore_errors=True)
"""
# Probe 2: the original faulthandler hang site. Importing scheduler.background
# in isolation must stay fully decoupled from the ML stack — not even
# transformers/torch at module load. NOTE: #4829 already decoupled the
# scheduler -> RAG service -> splitter chain, so a regression in
# text_splitter_registry is caught by Probe 1, not here. This probe guards the
# scheduler module's OWN import surface — it trips if a NEW heavy import lands
# directly on scheduler.background's chain (a tighter, non-redundant contract).
_PROBE_SCHEDULER = """
import sys
import local_deep_research.scheduler.background # noqa: F401
heavy = ("transformers", "sentence_transformers", "torch", "langchain_text_splitters")
loaded = sorted(m for m in heavy if m in sys.modules)
if loaded:
print("SCHEDULER_IMPORT_HEAVY:" + ",".join(loaded))
sys.exit(1)
print("SCHEDULER_IMPORT_CLEAN")
"""
def _run(probe: str) -> subprocess.CompletedProcess:
# Keep this comfortably below pyproject's global pytest timeout (180s,
# thread method). If a probe ever genuinely hangs, we want subprocess.run
# to raise a clean TimeoutExpired here — yielding a localized failure with
# the partial output — rather than pytest-timeout firing first and
# hard-killing the whole xdist worker (losing its other tests/coverage).
try:
return subprocess.run(
[sys.executable, "-c", probe],
capture_output=True,
text=True,
timeout=120,
)
except subprocess.TimeoutExpired as exc:
pytest.fail(
"Startup-import probe did not finish within 120s — likely a hang on "
f"the app-startup path.\npartial stdout:\n{exc.stdout}\n"
f"partial stderr:\n{exc.stderr}"
)
def test_create_app_does_not_import_splitter_stack():
"""Running create_app() must not load sentence-transformers / splitters."""
result = _run(_PROBE_CREATE_APP)
assert (
result.returncode == 0 and "CREATE_APP_SPLITTER_CLEAN" in result.stdout
), (
"create_app() pulled the sentence-transformers / langchain-text-splitters "
"stack onto the startup path. This reintroduces the startup-watchdog hang "
"fixed in #4829 — defer the offending top-level import to its call "
f"site.\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
def test_scheduler_import_is_ml_free():
"""Importing scheduler.background (the original hang site) loads no ML stack."""
result = _run(_PROBE_SCHEDULER)
assert (
result.returncode == 0 and "SCHEDULER_IMPORT_CLEAN" in result.stdout
), (
"Importing local_deep_research.scheduler.background pulled in the ML stack. "
"Keep the splitter import lazy (inside get_text_splitter) — and any RAG "
"service import out of module scope — so the scheduler import on the "
f"startup critical path stays light.\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)