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.
This commit is contained in:
byhow
2026-10-07 12:38:53 -07:00
parent 329c6af81e
commit 2ced7a9ddf
2 changed files with 26 additions and 4 deletions
@@ -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
+19 -4
View File
@@ -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)
)