Skip to content

Commit c819410

Browse files
committed
test(workflow): cover use_sub_branch parallel single_turn context isolation
Adds a regression for #7488: parametrize include_contents none/default and assert worker_b keeps its own node_input when parallel single_turn nodes run with use_sub_branch=True (A finishes before B builds context).
1 parent 3800f06 commit c819410

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

‎tests/unittests/workflow/test_llm_agent_as_node.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,82 @@ async def fanout(ctx: Context) -> dict[str, Any]:
21512151
assert not any('SECRET_FOR_A' in t for t in texts_b)
21522152

21532153

2154+
@pytest.mark.parametrize('include_contents', ['none', 'default'])
2155+
async def test_parallel_single_turn_nodes_with_sub_branch_see_own_node_input(
2156+
request: pytest.FixtureRequest,
2157+
include_contents: str,
2158+
):
2159+
"""use_sub_branch=True keeps each parallel single_turn node's own node_input.
2160+
2161+
Main already isolates unbranched parallel single_turn nodes by node path
2162+
(097ae1ea3). This covers the branched workaround: with use_sub_branch=True,
2163+
worker_b must still see INPUT_B under include_contents 'none' and 'default'.
2164+
On 2.11.0 without the sub-branch, worker_b lost INPUT_B and only saw
2165+
worker_a's reply when A finished first.
2166+
"""
2167+
a_finished = _asyncio.Event()
2168+
2169+
async def wait_for_a(callback_context: Context) -> None:
2170+
del callback_context
2171+
await a_finished.wait()
2172+
2173+
async def mark_a_finished(callback_context: Context) -> None:
2174+
del callback_context
2175+
a_finished.set()
2176+
2177+
model_a = _testing_utils.MockModel.create(responses=['out-a'])
2178+
model_b = _testing_utils.MockModel.create(responses=['out-b'])
2179+
worker_a = LlmAgent(
2180+
name='worker_a',
2181+
model=model_a,
2182+
instruction='Worker A.',
2183+
mode=AgentMode.SINGLE_TURN,
2184+
include_contents=include_contents,
2185+
after_agent_callback=mark_a_finished,
2186+
)
2187+
worker_b = LlmAgent(
2188+
name='worker_b',
2189+
model=model_b,
2190+
instruction='Worker B.',
2191+
mode=AgentMode.SINGLE_TURN,
2192+
include_contents=include_contents,
2193+
before_agent_callback=wait_for_a,
2194+
)
2195+
2196+
@node(rerun_on_resume=True)
2197+
async def fanout(ctx: Context) -> dict[str, Any]:
2198+
res_a, res_b = await _asyncio.gather(
2199+
ctx.run_node(
2200+
worker_a, node_input='INPUT_A', run_id='a', use_sub_branch=True
2201+
),
2202+
ctx.run_node(
2203+
worker_b, node_input='INPUT_B', run_id='b', use_sub_branch=True
2204+
),
2205+
)
2206+
return {'a': res_a, 'b': res_b}
2207+
2208+
wf = Workflow(name='parallel_sub_branch_wf', edges=[(START, fanout)])
2209+
runner = _new_workflow_runner(wf, request.function.__name__)
2210+
await runner.run_async(_testing_utils.get_user_content('start'))
2211+
2212+
assert len(model_a.requests) == 1
2213+
texts_a = [
2214+
p.text for p in _request_parts(model_a.requests[0]) if p.text
2215+
]
2216+
assert any('INPUT_A' in t for t in texts_a)
2217+
assert not any('INPUT_B' in t for t in texts_a)
2218+
assert not any('out-b' in t for t in texts_a)
2219+
2220+
assert len(model_b.requests) == 1
2221+
texts_b = [
2222+
p.text for p in _request_parts(model_b.requests[0]) if p.text
2223+
]
2224+
# Primary regression: worker_b must keep its own node_input.
2225+
assert any('INPUT_B' in t for t in texts_b)
2226+
assert not any('INPUT_A' in t for t in texts_b)
2227+
assert not any('out-a' in t for t in texts_b)
2228+
2229+
21542230
def _request_parts(request_obj: Any) -> list[types.Part]:
21552231
parts: list[types.Part] = []
21562232
for content in request_obj.contents:

0 commit comments

Comments
 (0)