merge: fix 多章链 checkpointer 连接串剥离驱动后缀(ProgrammingError 修复 + 回归测试)

This commit is contained in:
Yaojia Wang
2026-06-24 13:03:58 +02:00
2 changed files with 57 additions and 2 deletions

View File

@@ -0,0 +1,42 @@
"""chain_deps._libpq_conn_string 单测——剥离 SQLAlchemy 驱动后缀(回归)。
bugruntime checkpointer 工厂曾把 `postgresql+psycopg://…`settings 的 sync URL
原样喂给 `AsyncPostgresSaver.from_conn_string`psycopg 不识别 `+psycopg` 方言段 →
`ProgrammingError: missing "=" …` → 多章链 job 直接 failed。E2E 用 MemorySaver 覆盖,
从不走真 conn string故此路径无回归保护。本用例锁住「剥离后缀」语义。
"""
from __future__ import annotations
import pytest
from ww_api.services import chain_deps
class _StubSettings:
def __init__(self, url: str) -> None:
self.database_url_sync = url
@pytest.mark.parametrize(
("raw", "expected"),
[
(
"postgresql+psycopg://writer:writer@localhost:5432/writer",
"postgresql://writer:writer@localhost:5432/writer",
),
("postgresql+asyncpg://u:p@h:5432/db", "postgresql://u:p@h:5432/db"),
("postgresql://u:p@h:5432/db", "postgresql://u:p@h:5432/db"),
],
)
def test_libpq_conn_string_strips_driver_suffix(
monkeypatch: pytest.MonkeyPatch, raw: str, expected: str
) -> None:
# Arrange
monkeypatch.setattr(chain_deps, "get_settings", lambda: _StubSettings(raw))
# Act
result = chain_deps._libpq_conn_string()
# Assert
assert result == expected
assert "+psycopg" not in result
assert "+asyncpg" not in result

View File

@@ -28,6 +28,18 @@ if TYPE_CHECKING:
CheckpointerFactory = Callable[[], AbstractAsyncContextManager[Any]] CheckpointerFactory = Callable[[], AbstractAsyncContextManager[Any]]
def _libpq_conn_string() -> str:
"""从 settings 的 sync URL 派生 psycopg 的 libpq conn string。
`database_url_sync` 带 SQLAlchemy 驱动后缀(``postgresql+psycopg://…``),但
`AsyncPostgresSaver.from_conn_string` 直接交给 psycopg 解析——psycopg 不识别
``+psycopg``/``+asyncpg`` 方言段,会抛 ``ProgrammingError: missing "=" …``。
故剥离后缀还原为标准 libpq 串(``postgresql://…``)。与迁移 `d3e4f5a6b7c8`
的 `_psycopg_conn_string()` 同义。
"""
return get_settings().database_url_sync.replace("+psycopg", "").replace("+asyncpg", "")
def get_checkpointer_factory() -> CheckpointerFactory: def get_checkpointer_factory() -> CheckpointerFactory:
"""运行时 checkpointer 工厂:建连 `DATABASE_URL` 的 `AsyncPostgresSaver` 上下文。 """运行时 checkpointer 工厂:建连 `DATABASE_URL` 的 `AsyncPostgresSaver` 上下文。
@@ -38,7 +50,8 @@ def get_checkpointer_factory() -> CheckpointerFactory:
def _factory() -> AbstractAsyncContextManager[BaseCheckpointSaver[Any]]: def _factory() -> AbstractAsyncContextManager[BaseCheckpointSaver[Any]]:
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
# langgraph 的 Postgres saver 走 psycopg(同步驱动串)用 sync 串建连接。 # langgraph 的 Postgres saver 走 psycopg(同步驱动串)须剥离 SQLAlchemy 驱动
return AsyncPostgresSaver.from_conn_string(get_settings().database_url_sync) # 后缀(+psycopg否则 psycopg 解析连接串报 ProgrammingError。
return AsyncPostgresSaver.from_conn_string(_libpq_conn_string())
return _factory return _factory