Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

451 lines
13 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for @node decorator and behavior."""
from __future__ import annotations
from unittest import mock
from google.adk.agents.base_agent import BaseAgent
from google.adk.agents.llm_agent import LlmAgent
from google.adk.apps import App
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.base_tool import BaseTool
from google.adk.workflow import FunctionNode
from google.adk.workflow import START
from google.adk.workflow._base_node import BaseNode
from google.adk.workflow._node import node
from google.adk.workflow._node import Node
from google.adk.workflow._parallel_worker import _ParallelWorker as ParallelWorker
from google.adk.workflow._retry_config import RetryConfig
from google.adk.workflow._tool_node import _ToolNode as ToolNode
from google.adk.workflow._workflow import Workflow
from google.genai import types
import pytest
from .. import testing_utils
ANY = mock.ANY
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _run_workflow(wf, message="start"):
"""Run a Workflow through Runner, return collected events."""
ss = InMemorySessionService()
runner = Runner(app_name="test", node=wf, session_service=ss)
session = await ss.create_session(app_name="test", user_id="u")
msg = types.Content(parts=[types.Part(text=message)], role="user")
events = []
async for event in runner.run_async(
user_id="u", session_id=session.id, new_message=msg
):
events.append(event)
return events, ss, session
def _output_by_node(events):
"""Extract (node_name_from_path, output) for child node events."""
results = []
for e in events:
if e.output is not None and e.node_info.path and "/" in e.node_info.path:
node_name = e.node_info.path.rsplit("/", 1)[-1]
if "@" in node_name:
node_name = node_name.rsplit("@", 1)[0]
results.append((node_name, e.output))
return results
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_node_decorator():
"""Tests that @node decorator can wrap a function and override its name."""
@node(name="decorated_node")
def my_func():
return "Hello from decorated_func"
assert my_func.name == "decorated_node"
wf = Workflow(
name="test_agent",
edges=[
(START, my_func),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ("decorated_node", "Hello from decorated_func") in by_node
def test_node_parallel_worker_instance():
"""Tests that node() can wrap a node in ParallelWorker."""
@node(parallel_worker=True)
def my_func(node_input):
return node_input
assert isinstance(my_func, ParallelWorker)
assert my_func.name == "my_func"
def other_func(x):
return x
parallel_node = node(other_func, parallel_worker=True)
assert isinstance(parallel_node, ParallelWorker)
assert parallel_node.name == "other_func"
@pytest.mark.asyncio
async def test_node_parallel_worker_execution():
"""Tests that a node with parallel_worker=True correctly processes inputs."""
@node(parallel_worker=True)
async def my_func(node_input):
return node_input * 2
async def producer_func() -> list[int]:
return [1, 2, 3]
wf = Workflow(
name="test_agent",
edges=[
(START, producer_func),
(producer_func, my_func),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ("producer_func", [1, 2, 3]) in by_node
assert ("my_func", [2, 4, 6]) in by_node
def test_node_decorator_rerun_on_resume():
"""Tests that @node decorator can override rerun_on_resume."""
@node(name="decorated_node", rerun_on_resume=True)
def my_func():
return "Hello from decorated_func"
assert isinstance(my_func, FunctionNode)
assert my_func.rerun_on_resume
@node()
def my_func2():
return "Hello from decorated_func2"
assert isinstance(my_func2, FunctionNode)
assert not my_func2.rerun_on_resume
def test_node_decorator_parameter_binding():
"""Tests that @node decorator can configure parameter_binding."""
@node(parameter_binding="node_input")
def my_func(foo: str):
return f"Hello {foo}"
assert isinstance(my_func, FunctionNode)
assert my_func.parameter_binding == "node_input"
@pytest.mark.asyncio
async def test_node_decorator_parameter_binding_execution():
"""Tests execution of a node with parameter_binding='node_input'."""
async def producer_func() -> dict[str, Any]:
return {"foo": "hello", "bar": 42}
@node(parameter_binding="node_input")
def my_func(foo: str, bar: int):
return f"{foo}:{bar}"
wf = Workflow(
name="test_agent",
edges=[
(START, producer_func),
(producer_func, my_func),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ("my_func", "hello:42") in by_node
def test_node_function_with_base_node():
"""Tests that node() function returns a copied node when given a BaseNode."""
@node(name="original")
def original():
pass
wrapped = node(original, name="overridden", rerun_on_resume=True)
assert isinstance(wrapped, FunctionNode)
assert wrapped is not original
assert wrapped.name == "overridden"
assert wrapped.rerun_on_resume
class MyTool(BaseTool):
name = "tool"
description = "desc"
async def _run_async_impl(self):
return "done"
def test_node_no_unnecessary_wrap():
"""Tests that node() does not wrap LlmAgent, Agent, Tool, or func in OverridingNode."""
llm_agent = LlmAgent(name="llm")
llm_node = node(llm_agent, name="overridden_llm")
assert isinstance(llm_node, LlmAgent)
assert llm_node.name == "overridden_llm"
assert llm_node.mode == "single_turn"
agent = BaseAgent(name="agent")
agent_node_inst = node(agent, name="overridden_agent", rerun_on_resume=True)
assert isinstance(agent_node_inst, BaseAgent)
assert agent_node_inst.name == "overridden_agent"
assert agent_node_inst.rerun_on_resume
tool_inst = MyTool(name="tool", description="desc")
t_node = node(tool_inst, name="overridden_tool")
assert isinstance(t_node, ToolNode)
assert t_node.name == "overridden_tool"
def my_func():
pass
f_node = node(my_func, name="overridden_func", rerun_on_resume=True)
assert isinstance(f_node, FunctionNode)
assert f_node.name == "overridden_func"
assert f_node.rerun_on_resume
class StatefulTool(BaseTool):
"""A tool that modifies state via tool_context."""
async def run_async(self, *, args, tool_context):
tool_context.state["tool_key"] = "tool_value"
tool_context.state["tool_count"] = 10
return {"status": "ok"}
from .workflow_testing_utils import simplify_events_with_node
@pytest.mark.asyncio
async def test_tool_node_state_delta():
"""Tests that state set via tool_context.state in ToolNode is persisted."""
tool_node = ToolNode(
tool=StatefulTool(name="stateful_tool", description="Sets state values"),
)
def read_state(tool_key: str, tool_count: int) -> str:
return f"tool_key={tool_key}, tool_count={tool_count}"
def start_node():
return {}
wf = Workflow(
name="test_tool_node_state_delta",
edges=[
(START, start_node),
(start_node, tool_node),
(tool_node, read_state),
],
)
events, _, _ = await _run_workflow(wf)
simplified = simplify_events_with_node(
events, include_workflow_output=True, include_state_delta=True
)
assert (
"test_tool_node_state_delta@1/stateful_tool@1",
{"output": {"status": "ok"}},
) in [(e[0], {"output": e[1].get("output")}) for e in simplified]
assert (
"test_tool_node_state_delta@1/read_state@1",
{
"output": "tool_key=tool_value, tool_count=10",
},
) in [(e[0], {"output": e[1].get("output")}) for e in simplified]
class _CustomNode(Node):
custom_val: str = "hello"
rerun_on_resume: bool = True
async def run_node_impl(self, *, ctx, node_input):
yield f"subclass: {self.custom_val} -> {node_input}"
def test_node_subclassing_model_copy_preserves_identity():
"""Tests that Node.model_copy preserves the subclass class identity."""
node_inst = _CustomNode(
name="subclass", parallel_worker=True, custom_val="barrier"
)
assert node_inst.parallel_worker is True
cloned = node_inst.model_copy()
assert isinstance(cloned, _CustomNode)
assert cloned.custom_val == "barrier"
assert cloned.parallel_worker is True
assert isinstance(cloned._inner_node, ParallelWorker)
# Confirm inner node wraps a clone of _CustomNode preserving identity!
assert isinstance(cloned._inner_node._node, _CustomNode)
assert cloned._inner_node._node.custom_val == "barrier"
assert cloned._inner_node._node.parallel_worker is False
@pytest.mark.asyncio
async def test_node_subclassing_execution_with_parallel_worker():
"""Tests that a subclassed Node with parallel_worker=True executes successfully."""
subclass_node = _CustomNode(
name="subclass", parallel_worker=True, custom_val="workflow"
)
async def producer():
return ["input1", "input2"]
wf = Workflow(
name="test_agent",
edges=[
(START, producer),
(producer, subclass_node),
],
)
events, _, _ = await _run_workflow(wf)
by_node = _output_by_node(events)
assert ("producer", ["input1", "input2"]) in by_node
assert (
"subclass",
["subclass: workflow -> input1", "subclass: workflow -> input2"],
) in by_node
def test_node_decorator_parallel_worker_max_parallel_workers():
"""Tests that node() correctly sets max_parallel_workers on ParallelWorker."""
@node(parallel_worker=True, max_parallel_workers=3)
def my_func(node_input):
return node_input
assert isinstance(my_func, ParallelWorker)
assert my_func.max_parallel_workers == 3
def test_node_decorator_invalid_max_parallel_workers():
"""Tests that node() raises ValueError if max_parallel_workers is set without parallel_worker."""
with pytest.raises(
ValueError,
match="max_parallel_workers can only be set when parallel_worker is True",
):
@node(parallel_worker=False, max_parallel_workers=3)
def my_func(node_input):
return node_input
def test_node_subclass_invalid_max_parallel_workers():
"""Tests that Node subclass raises ValidationError if max_parallel_workers is set without parallel_worker."""
from pydantic import ValidationError
with pytest.raises(ValidationError) as exc_info:
_CustomNode(name="subclass", parallel_worker=False, max_parallel_workers=3)
assert (
"max_parallel_workers can only be set when parallel_worker is True"
in str(exc_info.value)
)
def test_node_subclassing_model_copy_preserves_max_parallel_workers():
"""Tests that Node.model_copy preserves max_parallel_workers."""
node_inst = _CustomNode(
name="subclass",
parallel_worker=True,
max_parallel_workers=5,
custom_val="barrier",
)
assert node_inst.parallel_worker is True
assert node_inst.max_parallel_workers == 5
assert node_inst._inner_node.max_parallel_workers == 5
cloned = node_inst.model_copy()
assert isinstance(cloned, _CustomNode)
assert cloned.parallel_worker is True
assert cloned.max_parallel_workers == 5
assert isinstance(cloned._inner_node, ParallelWorker)
assert cloned._inner_node.max_parallel_workers == 5
assert cloned._inner_node._node.parallel_worker is False
def test_node_decorator_invalid_max_parallel_workers_less_than_one():
"""Tests that node() raises ValueError if max_parallel_workers is less than 1."""
with pytest.raises(
ValueError,
match="max_parallel_workers must be greater than or equal to 1",
):
@node(parallel_worker=True, max_parallel_workers=0)
def my_func(node_input):
return node_input
def test_node_subclass_invalid_max_parallel_workers_less_than_one():
"""Tests that Node subclass raises ValidationError if max_parallel_workers is less than 1."""
from pydantic import ValidationError
with pytest.raises(ValidationError) as exc_info:
_CustomNode(name="subclass", parallel_worker=True, max_parallel_workers=0)
assert "max_parallel_workers must be greater than or equal to 1" in str(
exc_info.value
)
def test_parallel_worker_invalid_max_parallel_workers_less_than_one():
"""Tests that ParallelWorker constructor raises ValueError if max_parallel_workers is less than 1."""
def dummy_func(x):
return x
with pytest.raises(
ValueError,
match="max_parallel_workers must be greater than or equal to 1",
):
ParallelWorker(node=dummy_func, max_parallel_workers=0)