From 2ced7a9ddf9bb3d4f6efdbe967654fcdcf8bfd10 Mon Sep 17 00:00:00 2001 From: byhow <25713361+byhow@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:38:53 -0700 Subject: [PATCH] test(langgraph,prebuilt): cover wrapped ToolNodes and errors is_invalid_resume ignores The nested-interrupt tests also run with a pass-through `wrap_tool_call` / `awrap_tool_call`, the path `create_agent` always takes. A new test checks that `is_invalid_resume` is False for an ordinary `ValidationError` and for other errors. --- libs/langgraph/tests/test_interruption.py | 7 +++++++ libs/prebuilt/tests/test_tool_node.py | 23 +++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index 983c172dc..50b8ed6d0 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -212,6 +212,13 @@ def test_interrupt_response_schema_rejects_invalid_resume( } +def test_is_invalid_resume_ignores_other_errors() -> None: + with pytest.raises(ValidationError) as exc_info: + Decision.model_validate({"approved": "nope"}) + assert not is_invalid_resume(exc_info.value) + assert not is_invalid_resume(ValueError("nope")) + + @pytest.mark.parametrize("resume_style", ["null", "id_map"]) def test_interrupt_response_schema_invalid_resume_after_earlier_interrupt( sync_checkpointer: BaseCheckpointSaver, resume_style: str diff --git a/libs/prebuilt/tests/test_tool_node.py b/libs/prebuilt/tests/test_tool_node.py index 0557c3b62..b4e8b39f9 100644 --- a/libs/prebuilt/tests/test_tool_node.py +++ b/libs/prebuilt/tests/test_tool_node.py @@ -650,8 +650,9 @@ def _ask_human_call() -> dict[str, list[AnyMessage]]: return {"messages": [AIMessage("", tool_calls=[call])]} +@pytest.mark.parametrize("wrapped", [False, True], ids=["plain", "wrapped"]) def test_tool_node_reraises_invalid_resume_from_nested_interrupt( - sync_checkpointer: BaseCheckpointSaver, + sync_checkpointer: BaseCheckpointSaver, wrapped: bool ) -> None: asker = _approval_graph() @@ -660,9 +661,16 @@ def test_tool_node_reraises_invalid_resume_from_nested_interrupt( """Ask a human for approval.""" return asker.invoke({})["answer"] + # `create_agent` always runs tools through a wrapper (its middleware). + def pass_through(request, handler): + return handler(request) + graph = ( StateGraph(MessagesState) - .add_node("tools", ToolNode([ask_human])) + .add_node( + "tools", + ToolNode([ask_human], wrap_tool_call=pass_through if wrapped else None), + ) .add_edge(START, "tools") .compile(checkpointer=sync_checkpointer) ) @@ -679,8 +687,9 @@ def test_tool_node_reraises_invalid_resume_from_nested_interrupt( assert result["messages"][-1].content == "approved=True" +@pytest.mark.parametrize("wrapped", [False, True], ids=["plain", "wrapped"]) async def test_tool_node_reraises_invalid_resume_from_nested_interrupt_async( - async_checkpointer: BaseCheckpointSaver, + async_checkpointer: BaseCheckpointSaver, wrapped: bool ) -> None: asker = _approval_graph() @@ -689,9 +698,15 @@ async def test_tool_node_reraises_invalid_resume_from_nested_interrupt_async( """Ask a human for approval.""" return (await asker.ainvoke({}))["answer"] + async def pass_through(request, handler): + return await handler(request) + graph = ( StateGraph(MessagesState) - .add_node("tools", ToolNode([ask_human])) + .add_node( + "tools", + ToolNode([ask_human], awrap_tool_call=pass_through if wrapped else None), + ) .add_edge(START, "tools") .compile(checkpointer=async_checkpointer) )