Files
openai--openai-agents-python/tests/test_output_guardrail_cancellation.py
2026-07-13 12:39:17 +08:00

67 lines
2.4 KiB
Python

from __future__ import annotations
import asyncio
from typing import Any
import pytest
from agents import (
Agent,
GuardrailFunctionOutput,
OutputGuardrail,
OutputGuardrailTripwireTriggered,
RunContextWrapper,
)
from agents.run_internal.guardrails import run_output_guardrails
@pytest.mark.asyncio
async def test_run_output_guardrails_awaits_cancelled_tasks():
"""When one output guardrail trips, sibling guardrails must be awaited after cancel.
Regression test: ``run_output_guardrails`` previously cancelled sibling tasks but
did not await them, which can leak pending tasks and emit
``Task was destroyed but it is pending!`` warnings. The input-guardrail variants
already await on cancel; the output variant should match.
"""
slow_started = asyncio.Event()
cancelled_observed = asyncio.Event()
async def slow_then_observe_cancel(
context: RunContextWrapper[Any], agent: Agent[Any], agent_output: Any
) -> GuardrailFunctionOutput:
slow_started.set()
try:
await asyncio.sleep(5)
except asyncio.CancelledError:
cancelled_observed.set()
raise
return GuardrailFunctionOutput(output_info=None, tripwire_triggered=False)
async def fast_tripwire(
context: RunContextWrapper[Any], agent: Agent[Any], agent_output: Any
) -> GuardrailFunctionOutput:
# Wait until the slow guardrail is actually parked on its sleep so the
# subsequent cancel hits the installed CancelledError handler. Without
# this, the slow task could be cancelled before reaching the try block
# and ``cancelled_observed`` would stay unset even with the production
# fix in place.
await slow_started.wait()
return GuardrailFunctionOutput(output_info=None, tripwire_triggered=True)
guardrails = [
OutputGuardrail(guardrail_function=slow_then_observe_cancel),
OutputGuardrail(guardrail_function=fast_tripwire),
]
agent: Agent[Any] = Agent(name="test")
context: RunContextWrapper[Any] = RunContextWrapper(context=None)
with pytest.raises(OutputGuardrailTripwireTriggered):
await run_output_guardrails(guardrails, agent, "agent output", context)
# The slow guardrail must have observed cancellation and finished before
# ``run_output_guardrails`` returned. If it had not been awaited, this event
# would still be unset at this point.
assert cancelled_observed.is_set()