diff --git a/backend/packages/harness/deerflow/config/checkpointer_config.py b/backend/packages/harness/deerflow/config/checkpointer_config.py index 0afe6a485..963c439c1 100644 --- a/backend/packages/harness/deerflow/config/checkpointer_config.py +++ b/backend/packages/harness/deerflow/config/checkpointer_config.py @@ -14,12 +14,13 @@ class CheckpointerConfig(BaseModel): description="Checkpointer backend type. " "'memory' is in-process only (lost on restart). " "'sqlite' persists to a local file (requires langgraph-checkpoint-sqlite). " - "'postgres' persists to PostgreSQL (requires langgraph-checkpoint-postgres)." + "'postgres' persists to PostgreSQL (install with deerflow-harness[postgres])." ) connection_string: str | None = Field( default=None, description="Connection string for sqlite (file path) or postgres (DSN). " - "Required for sqlite and postgres types. " + "Optional for sqlite and defaults to 'store.db' when omitted. " + "Required for postgres. " "For sqlite, use a file path like '.deer-flow/checkpoints.db' or ':memory:' for in-memory. " "For postgres, use a DSN like 'postgresql://user:pass@localhost:5432/db'.", ) diff --git a/backend/packages/harness/deerflow/persistence/engine.py b/backend/packages/harness/deerflow/persistence/engine.py index 2777c2450..5ed647d03 100644 --- a/backend/packages/harness/deerflow/persistence/engine.py +++ b/backend/packages/harness/deerflow/persistence/engine.py @@ -81,7 +81,9 @@ async def init_engine( try: import asyncpg # noqa: F401 except ImportError: - raise ImportError("database.backend is set to 'postgres' but asyncpg is not installed.\nInstall it with:\n uv sync --extra postgres\nOr switch to backend: sqlite in config.yaml for single-node deployment.") from None + raise ImportError( + "database.backend is set to 'postgres' but asyncpg is not installed.\nInstall it with:\n uv sync --all-packages --extra postgres\nOr switch to backend: sqlite in config.yaml for single-node deployment." + ) from None if backend == "sqlite": import os diff --git a/backend/packages/harness/deerflow/runtime/checkpointer/provider.py b/backend/packages/harness/deerflow/runtime/checkpointer/provider.py index 5ee66be83..39a4f272e 100644 --- a/backend/packages/harness/deerflow/runtime/checkpointer/provider.py +++ b/backend/packages/harness/deerflow/runtime/checkpointer/provider.py @@ -36,7 +36,9 @@ logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- SQLITE_INSTALL = "langgraph-checkpoint-sqlite is required for the SQLite checkpointer. Install it with: uv add langgraph-checkpoint-sqlite" -POSTGRES_INSTALL = "langgraph-checkpoint-postgres is required for the PostgreSQL checkpointer. Install it with: uv add langgraph-checkpoint-postgres psycopg[binary] psycopg-pool" +POSTGRES_INSTALL = ( + "langgraph-checkpoint-postgres is required for the PostgreSQL checkpointer. Install the package extra with: pip install 'deerflow-harness[postgres]' (or use: uv sync --all-packages --extra postgres when developing locally)" +) POSTGRES_CONN_REQUIRED = "checkpointer.connection_string is required for the postgres backend" # --------------------------------------------------------------------------- diff --git a/backend/packages/harness/deerflow/runtime/store/provider.py b/backend/packages/harness/deerflow/runtime/store/provider.py index a9394fb9f..ecf597fe3 100644 --- a/backend/packages/harness/deerflow/runtime/store/provider.py +++ b/backend/packages/harness/deerflow/runtime/store/provider.py @@ -36,7 +36,9 @@ logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- SQLITE_STORE_INSTALL = "langgraph-checkpoint-sqlite is required for the SQLite store. Install it with: uv add langgraph-checkpoint-sqlite" -POSTGRES_STORE_INSTALL = "langgraph-checkpoint-postgres is required for the PostgreSQL store. Install it with: uv add langgraph-checkpoint-postgres psycopg[binary] psycopg-pool" +POSTGRES_STORE_INSTALL = ( + "langgraph-checkpoint-postgres is required for the PostgreSQL store. Install the package extra with: pip install 'deerflow-harness[postgres]' (or use: uv sync --all-packages --extra postgres when developing locally)" +) POSTGRES_CONN_REQUIRED = "checkpointer.connection_string is required for the postgres backend" # --------------------------------------------------------------------------- diff --git a/backend/tests/test_checkpointer.py b/backend/tests/test_checkpointer.py index 5a31cfb78..e7714e3ce 100644 --- a/backend/tests/test_checkpointer.py +++ b/backend/tests/test_checkpointer.py @@ -1,6 +1,8 @@ -"""Unit tests for checkpointer config and singleton factory.""" +"""Unit tests for checkpointer config, packaging metadata, and factories.""" import sys +import tomllib +from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -13,6 +15,8 @@ from deerflow.config.checkpointer_config import ( set_checkpointer_config, ) from deerflow.runtime.checkpointer import get_checkpointer, reset_checkpointer +from deerflow.runtime.checkpointer.provider import POSTGRES_INSTALL +from deerflow.runtime.store.provider import POSTGRES_STORE_INSTALL @pytest.fixture(autouse=True) @@ -67,6 +71,42 @@ class TestCheckpointerConfig: with pytest.raises(Exception): load_checkpointer_config_from_dict({"type": "unknown"}) + def test_connection_string_description_matches_runtime_defaults(self): + description = CheckpointerConfig.model_fields["connection_string"].description + + assert description is not None + assert "Optional for sqlite" in description + assert "defaults to 'store.db'" in description + assert "Required for postgres" in description + + +class TestHarnessPackaging: + def test_pyproject_declares_postgres_extra(self): + pyproject_path = Path(__file__).resolve().parents[1] / "packages" / "harness" / "pyproject.toml" + data = tomllib.loads(pyproject_path.read_text()) + + optional_dependencies = data["project"]["optional-dependencies"] + assert "postgres" in optional_dependencies + assert optional_dependencies["postgres"] == [ + "asyncpg>=0.29", + "langgraph-checkpoint-postgres>=3.0.5", + "psycopg[binary]>=3.3.3", + "psycopg-pool>=3.3.0", + ] + + def test_workspace_pyproject_forwards_postgres_extra_to_harness(self): + pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml" + data = tomllib.loads(pyproject_path.read_text()) + + optional_dependencies = data["project"]["optional-dependencies"] + assert optional_dependencies["postgres"] == ["deerflow-harness[postgres]"] + + def test_postgres_missing_dependency_messages_recommend_package_extra(self): + assert "deerflow-harness[postgres]" in POSTGRES_INSTALL + assert "deerflow-harness[postgres]" in POSTGRES_STORE_INSTALL + assert "uv sync --all-packages --extra postgres" in POSTGRES_INSTALL + assert "uv sync --all-packages --extra postgres" in POSTGRES_STORE_INSTALL + # --------------------------------------------------------------------------- # Factory tests diff --git a/backend/tests/test_persistence_scaffold.py b/backend/tests/test_persistence_scaffold.py index 178a08e84..e0bca3eef 100644 --- a/backend/tests/test_persistence_scaffold.py +++ b/backend/tests/test_persistence_scaffold.py @@ -8,7 +8,9 @@ Tests: 5. Postgres missing-dep error message """ +import sys from datetime import UTC, datetime +from unittest.mock import patch import pytest @@ -221,13 +223,8 @@ class TestEngineLifecycle: """If asyncpg is not installed, error message tells user what to do.""" from deerflow.persistence.engine import init_engine - try: - import asyncpg # noqa: F401 - - pytest.skip("asyncpg is installed -- cannot test missing-dep path") - except ImportError: - # asyncpg is not installed — this is the expected state for this test. - # We proceed to verify that init_engine raises an actionable ImportError. - pass # noqa: S110 — intentionally ignored - with pytest.raises(ImportError, match="uv sync --extra postgres"): + with ( + patch.dict(sys.modules, {"asyncpg": None}), + pytest.raises(ImportError, match="uv sync --all-packages --extra postgres"), + ): await init_engine("postgres", url="postgresql+asyncpg://x:x@localhost/x")