From 60c9602bccfc2f50e3f84be57edbb6bf78d77d48 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 13 Jan 2026 05:36:10 -0800 Subject: [PATCH] add test --- .github/workflows/_test_langgraph.yml | 43 ++++++++++++++++ libs/langgraph/tests/conftest_checkpointer.py | 49 +++++++++++++++++-- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_test_langgraph.yml b/.github/workflows/_test_langgraph.yml index 3aee8b3f9..f8d337d86 100644 --- a/.github/workflows/_test_langgraph.yml +++ b/.github/workflows/_test_langgraph.yml @@ -56,3 +56,46 @@ jobs: # grep will exit non-zero if the target message isn't found, # and `set -e` above will cause the step to fail. echo "$STATUS" | grep 'nothing to commit, working tree clean' + + build-strict-msgpack: + runs-on: ubuntu-latest + defaults: + run: + working-directory: libs/langgraph + name: "test strict msgpack #3.12" + steps: + - uses: actions/checkout@v6 + - name: Set up Python 3.12 + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + enable-cache: true + cache-suffix: "test-langgraph-strict-msgpack" + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_RO_TOKEN }} + + - name: Install dependencies + shell: bash + run: uv sync --frozen --group test --no-dev + + - name: Run tests (strict msgpack) + shell: bash + env: + LANGGRAPH_STRICT_MSGPACK: "1" + run: make test_parallel + + - name: Ensure the tests did not create any additional files + shell: bash + run: | + set -eu + + STATUS="$(git status)" + echo "$STATUS" + + # grep will exit non-zero if the target message isn't found, + # and `set -e` above will cause the step to fail. + echo "$STATUS" | grep 'nothing to commit, working tree clean' diff --git a/libs/langgraph/tests/conftest_checkpointer.py b/libs/langgraph/tests/conftest_checkpointer.py index 99c0686fc..b74e55923 100644 --- a/libs/langgraph/tests/conftest_checkpointer.py +++ b/libs/langgraph/tests/conftest_checkpointer.py @@ -1,3 +1,4 @@ +import os from contextlib import asynccontextmanager, contextmanager from uuid import uuid4 @@ -5,6 +6,7 @@ import pytest from langgraph.checkpoint.postgres import PostgresSaver from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from langgraph.checkpoint.serde.encrypted import EncryptedSerializer +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver from psycopg import AsyncConnection, Connection @@ -18,30 +20,60 @@ from tests.memory_assert import ( # noqa: E402 ) DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/" +STRICT_MSGPACK = os.getenv("LANGGRAPH_STRICT_MSGPACK", "false").lower() in ( + "1", + "true", + "yes", +) + + +def _strict_msgpack_serde() -> JsonPlusSerializer: + return JsonPlusSerializer(allowed_msgpack_modules=None) + + +def _apply_strict_msgpack(checkpointer) -> None: + if not STRICT_MSGPACK: + return + serde = _strict_msgpack_serde() + if hasattr(checkpointer, "serde"): + checkpointer.serde = serde + if hasattr(checkpointer, "saver") and hasattr(checkpointer.saver, "serde"): + checkpointer.saver.serde = serde @contextmanager def _checkpointer_memory(): - yield MemorySaverAssertImmutable() + if STRICT_MSGPACK: + yield MemorySaverAssertImmutable(serde=_strict_msgpack_serde()) + else: + yield MemorySaverAssertImmutable() @contextmanager def _checkpointer_memory_migrate_sends(): - yield MemorySaverNeedsPendingSendsMigration() + checkpointer = MemorySaverNeedsPendingSendsMigration() + _apply_strict_msgpack(checkpointer) + yield checkpointer @contextmanager def _checkpointer_sqlite(): with SqliteSaver.from_conn_string(":memory:") as checkpointer: + _apply_strict_msgpack(checkpointer) yield checkpointer @contextmanager def _checkpointer_sqlite_aes(): with SqliteSaver.from_conn_string(":memory:") as checkpointer: - checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes( - key=b"1234567890123456" - ) + if STRICT_MSGPACK: + checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes( + serde=_strict_msgpack_serde(), key=b"1234567890123456" + ) + else: + checkpointer.serde = EncryptedSerializer.from_pycryptodome_aes( + key=b"1234567890123456" + ) yield checkpointer @@ -57,6 +89,7 @@ def _checkpointer_postgres(): DEFAULT_POSTGRES_URI + database ) as checkpointer: checkpointer.setup() + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db @@ -79,6 +112,7 @@ def _checkpointer_postgres_pipe(): # setup can't run inside pipeline because of implicit transaction with checkpointer.conn.pipeline() as pipe: checkpointer.pipe = pipe + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db @@ -99,6 +133,7 @@ def _checkpointer_postgres_pool(): ) as pool: checkpointer = PostgresSaver(pool) checkpointer.setup() + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db @@ -109,6 +144,7 @@ def _checkpointer_postgres_pool(): @asynccontextmanager async def _checkpointer_sqlite_aio(): async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: + _apply_strict_msgpack(checkpointer) yield checkpointer @@ -126,6 +162,7 @@ async def _checkpointer_postgres_aio(): DEFAULT_POSTGRES_URI + database ) as checkpointer: await checkpointer.setup() + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db @@ -152,6 +189,7 @@ async def _checkpointer_postgres_aio_pipe(): # setup can't run inside pipeline because of implicit transaction async with checkpointer.conn.pipeline() as pipe: checkpointer.pipe = pipe + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db @@ -176,6 +214,7 @@ async def _checkpointer_postgres_aio_pool(): ) as pool: checkpointer = AsyncPostgresSaver(pool) await checkpointer.setup() + _apply_strict_msgpack(checkpointer) yield checkpointer finally: # drop unique db