Files
bytedance--deer-flow/backend/tests/blocking_io/test_persistence_engine_sqlite.py
T
2026-07-13 11:59:58 +08:00

78 lines
2.9 KiB
Python

"""Regression test: persistence-engine sqlite dir setup must run off the loop.
Anchors the production offload in `persistence/engine.py:init_engine`, where the
SQLite data directory is created with `os.makedirs`. `init_engine` runs on the
FastAPI lifespan event loop, so a sync `os.makedirs` (a stat + mkdir syscall)
there blocks startup — the same class of bug fixed for the checkpointer's
`ensure_sqlite_parent_dir` in #1912 (see `test_sqlite_lifespan.py`).
This invokes the production `init_engine(backend="sqlite", ...)` under the strict
Blockbuster context with a `sqlite_dir` that does not yet exist, so `os.makedirs`
actually runs. The async engine/session machinery is mocked out so the only host
filesystem operation under test is the directory creation; if it regresses to run
directly on the event loop, Blockbuster raises `BlockingError` and this fails.
We also stub ``bootstrap_schema`` so the alembic stamp/upgrade path -- which has
its own ``asyncio.to_thread`` regression anchor in
``test_persistence_bootstrap.py`` -- does not turn this test into a
double-coverage one. Keeping concerns separated means a regression in either
offload (makedirs vs alembic) points at the right place.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Pre-import so `init_engine`'s lazy ``import deerflow.persistence.models`` is a
# cached no-op rather than a file read under the strict gate.
import deerflow.persistence.models # noqa: E402,F401
from deerflow.persistence import engine as engine_mod # noqa: E402
pytestmark = pytest.mark.asyncio
def _noop_listens_for(*_args, **_kwargs):
"""Decorator factory that registers nothing (mock engine has no real events)."""
def _decorator(fn):
return fn
return _decorator
async def test_init_engine_sqlite_dir_setup_does_not_block_event_loop(tmp_path: Path) -> None:
data_dir = tmp_path / "newsubdir" # does not exist yet -> os.makedirs runs
db_file = data_dir / "app.db"
mock_conn = AsyncMock()
begin_ctx = AsyncMock()
begin_ctx.__aenter__.return_value = mock_conn
begin_ctx.__aexit__.return_value = False
mock_engine = MagicMock()
mock_engine.begin.return_value = begin_ctx
mock_engine.dispose = AsyncMock()
async def _noop_bootstrap(*_args, **_kwargs):
return None
with (
patch.object(engine_mod, "create_async_engine", return_value=mock_engine),
patch.object(engine_mod, "async_sessionmaker", return_value=MagicMock()),
patch("sqlalchemy.event.listens_for", _noop_listens_for),
patch(
"deerflow.persistence.bootstrap.bootstrap_schema",
new=_noop_bootstrap,
),
):
await engine_mod.init_engine(
backend="sqlite",
url=f"sqlite+aiosqlite:///{db_file}",
sqlite_dir=str(data_dir),
)
assert data_dir.exists()
await engine_mod.close_engine()