7dfc9968fe
apps/:新增「基于 DeerFlow 的应用」目录,与 backend/、frontend/ 平级, 位于「app 消费 deerflow、不反向依赖」边界的正确侧。含两种集成示例: - examples/http-chat —— HTTP Gateway (REST+SSE),含登录/CSRF/建线程/流式对话 - examples/embedded-chat —— 进程内直接调 DeerFlowClient README 说明边界规则、两种模式、鉴权流程及新建应用约定。 runtime/store:修复 make_store 缺失的 database 段回退。原先 store 工厂只读 legacy 的 checkpointer 段,导致仅配 database:postgres 时,checkpointer 走了 Postgres、但 store 仍回退 InMemoryStore(并打出误导性的「线程列表会丢失」告警, 实际线程在 threads_meta 表里、本就持久)。现对齐 checkpointer 工厂的优先级: checkpointer 段 → database 段 → InMemoryStore;postgres 分支同样剥掉 +asyncpg 方言前缀,使一个 DATABASE_URL 同时满足 SQLAlchemy 与 LangGraph 的 psycopg store。 告警文案也修正为「跨线程 store 数据会丢失」。 tests:新增 test_store_provider.py(3 例,TDD)覆盖 database→postgres 回退、 无配置时的内存回退、以及 checkpointer 段优先级。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.5 KiB
Python
90 lines
3.5 KiB
Python
"""Tests for the async Store factory's backend selection.
|
|
|
|
Mirrors the checkpointer factory: when no legacy ``checkpointer`` section
|
|
is configured but a unified ``database`` section is, the store must use
|
|
that database backend instead of silently falling back to InMemoryStore.
|
|
"""
|
|
|
|
import sys
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from deerflow.config.database_config import DatabaseConfig
|
|
from deerflow.runtime.store.async_provider import make_store
|
|
|
|
|
|
@pytest.fixture
|
|
def anyio_backend() -> str:
|
|
return "asyncio"
|
|
|
|
|
|
class TestStoreDatabaseFallback:
|
|
@pytest.mark.anyio
|
|
async def test_postgres_store_from_database_when_no_checkpointer_section(self):
|
|
"""make_store uses AsyncPostgresStore from the database section when
|
|
no legacy checkpointer section is present. The +asyncpg dialect prefix
|
|
is stripped so the same DATABASE_URL satisfies both SQLAlchemy and
|
|
LangGraph's psycopg-based store."""
|
|
mock_config = MagicMock()
|
|
mock_config.checkpointer = None
|
|
mock_config.database = DatabaseConfig(
|
|
backend="postgres",
|
|
postgres_url="postgresql+asyncpg://postgres:pw@localhost:5432/deerflow",
|
|
)
|
|
|
|
mock_store = AsyncMock()
|
|
mock_cm = AsyncMock()
|
|
mock_cm.__aenter__.return_value = mock_store
|
|
mock_cm.__aexit__.return_value = False
|
|
|
|
mock_store_cls = MagicMock()
|
|
mock_store_cls.from_conn_string.return_value = mock_cm
|
|
|
|
mock_module = MagicMock()
|
|
mock_module.AsyncPostgresStore = mock_store_cls
|
|
|
|
with (
|
|
patch("deerflow.runtime.store.async_provider.get_app_config", return_value=mock_config),
|
|
patch.dict(sys.modules, {"langgraph.store.postgres.aio": mock_module}),
|
|
):
|
|
async with make_store() as store:
|
|
assert store is mock_store
|
|
|
|
# dialect prefix stripped to libpq conninfo
|
|
mock_store_cls.from_conn_string.assert_called_once_with("postgresql://postgres:pw@localhost:5432/deerflow")
|
|
mock_store.setup.assert_awaited_once()
|
|
|
|
@pytest.mark.anyio
|
|
async def test_memory_when_no_checkpointer_and_no_database(self):
|
|
"""With neither a checkpointer section nor a non-memory database,
|
|
the store falls back to InMemoryStore."""
|
|
from langgraph.store.memory import InMemoryStore
|
|
|
|
mock_config = MagicMock()
|
|
mock_config.checkpointer = None
|
|
mock_config.database = DatabaseConfig(backend="memory")
|
|
|
|
with patch("deerflow.runtime.store.async_provider.get_app_config", return_value=mock_config):
|
|
async with make_store() as store:
|
|
assert isinstance(store, InMemoryStore)
|
|
|
|
@pytest.mark.anyio
|
|
async def test_checkpointer_section_takes_precedence_over_database(self):
|
|
"""A legacy checkpointer section still wins over the database section."""
|
|
from deerflow.config.checkpointer_config import CheckpointerConfig
|
|
|
|
mock_config = MagicMock()
|
|
mock_config.checkpointer = CheckpointerConfig(type="memory")
|
|
mock_config.database = DatabaseConfig(
|
|
backend="postgres",
|
|
postgres_url="postgresql+asyncpg://postgres:pw@localhost:5432/deerflow",
|
|
)
|
|
|
|
from langgraph.store.memory import InMemoryStore
|
|
|
|
with patch("deerflow.runtime.store.async_provider.get_app_config", return_value=mock_config):
|
|
async with make_store() as store:
|
|
# checkpointer.type == memory → InMemoryStore, database ignored
|
|
assert isinstance(store, InMemoryStore)
|