test(run_agent): keep concurrent-interrupt stub in sync with AIAgent surface
The stub in tests/run_agent/test_concurrent_interrupt.py mirrors a subset
of AIAgent attributes/methods deliberately ("Avoid full AIAgent init —
just import the class and build a stub"). Two recent additions to the
real AIAgent broke it:
1. Tool-call guardrails: AIAgent gained _tool_guardrails (line 1160) +
_append_guardrail_observation / _guardrail_block_result /
_set_tool_guardrail_halt that the concurrent-execution path now
invokes during result collection. Add a MagicMock guardrail
controller whose decision objects mirror the
ToolGuardrailDecision shape (action="allow", allows_execution=True,
should_halt=False) — the bound methods read sensible defaults
instead of truthy MagicMock children. Bind the three methods the
same way as other AIAgent methods.
2. _invoke_tool gained kwargs: messages= and pre_tool_block_checked=
are now forwarded by the concurrent path. The two test fakes
(slow_tool, polling_tool) didn't accept them and raised TypeError
on every call. Add **kwargs so future kwargs additions don't break
these fakes.
These are pure stub-drift fixes — production behaviour is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
74d5e5a899
commit
5f179d6d35
@ -47,12 +47,32 @@ def _make_agent(monkeypatch):
|
||||
# Worker-thread tracking state mirrored from AIAgent.__init__ so the
|
||||
# real interrupt() method can fan out to concurrent-tool workers.
|
||||
_active_children: list = []
|
||||
# ToolCallGuardrailController instance on real AIAgent. The stub
|
||||
# always allows execution — these tests exercise interrupt fanout,
|
||||
# not guardrail behaviour. before_call returns a decision-shaped
|
||||
# object with allows_execution=True; after_call is similar.
|
||||
_tool_guardrails = MagicMock()
|
||||
|
||||
def __init__(self):
|
||||
# Instance-level (not class-level) so each test gets a fresh set.
|
||||
self._tool_worker_threads: set = set()
|
||||
self._tool_worker_threads_lock = threading.Lock()
|
||||
self._active_children_lock = threading.Lock()
|
||||
# Re-assign per-instance so tests can mutate independently.
|
||||
self._tool_guardrails = MagicMock()
|
||||
# Decision shapes match agent.tool_guardrails.ToolGuardrailDecision —
|
||||
# action="allow", allows_execution=True, should_halt=False — so
|
||||
# the bound _append_guardrail_observation / _guardrail_block_result
|
||||
# methods read sensible defaults instead of truthy MagicMock children.
|
||||
_allow = MagicMock(
|
||||
action="allow",
|
||||
allows_execution=True,
|
||||
should_halt=False,
|
||||
code=None,
|
||||
count=0,
|
||||
)
|
||||
self._tool_guardrails.before_call.return_value = _allow
|
||||
self._tool_guardrails.after_call.return_value = _allow
|
||||
|
||||
def _touch_activity(self, desc):
|
||||
self._last_activity = time.time()
|
||||
@ -77,6 +97,21 @@ def _make_agent(monkeypatch):
|
||||
stub._execute_tool_calls_concurrent = _ra.AIAgent._execute_tool_calls_concurrent.__get__(stub)
|
||||
stub.interrupt = _ra.AIAgent.interrupt.__get__(stub)
|
||||
stub.clear_interrupt = _ra.AIAgent.clear_interrupt.__get__(stub)
|
||||
# Tool-guardrails wiring: the concurrent execution path now calls
|
||||
# _append_guardrail_observation / _guardrail_block_result /
|
||||
# _set_tool_guardrail_halt during the result-collection loop. These are
|
||||
# thin pass-throughs over self._tool_guardrails; bind them for the stub
|
||||
# the same way as other AIAgent methods.
|
||||
stub._append_guardrail_observation = (
|
||||
_ra.AIAgent._append_guardrail_observation.__get__(stub)
|
||||
)
|
||||
stub._guardrail_block_result = (
|
||||
_ra.AIAgent._guardrail_block_result.__get__(stub)
|
||||
)
|
||||
stub._set_tool_guardrail_halt = (
|
||||
_ra.AIAgent._set_tool_guardrail_halt.__get__(stub)
|
||||
)
|
||||
stub._tool_guardrail_halt_decision = None
|
||||
# /steer injection (added in PR #12116) fires after every concurrent
|
||||
# tool batch. Stub it as a no-op — this test exercises interrupt
|
||||
# fanout, not steer injection.
|
||||
@ -107,7 +142,11 @@ def test_concurrent_interrupt_cancels_pending(monkeypatch):
|
||||
|
||||
original_invoke = agent._invoke_tool
|
||||
|
||||
def slow_tool(name, args, task_id, call_id=None):
|
||||
def slow_tool(name, args, task_id, call_id=None, **kwargs):
|
||||
# **kwargs absorbs `messages=...` and `pre_tool_block_checked=...`
|
||||
# that _invoke_tool now forwards. The test only cares about the
|
||||
# positional shape; future kwargs added to _invoke_tool shouldn't
|
||||
# break this fake.
|
||||
if name == "slow_one":
|
||||
# Block until the test sets the interrupt
|
||||
barrier.wait(timeout=10)
|
||||
@ -184,7 +223,10 @@ def test_running_concurrent_worker_sees_is_interrupted(monkeypatch):
|
||||
observed = {"saw_true": False, "poll_count": 0, "worker_tid": None}
|
||||
worker_started = threading.Event()
|
||||
|
||||
def polling_tool(name, args, task_id, call_id=None, messages=None):
|
||||
def polling_tool(name, args, task_id, call_id=None, messages=None, **kwargs):
|
||||
# **kwargs absorbs pre_tool_block_checked and any future kwargs added
|
||||
# to _invoke_tool — see test_concurrent_interrupt_cancels_pending for
|
||||
# the same pattern.
|
||||
observed["worker_tid"] = threading.current_thread().ident
|
||||
worker_started.set()
|
||||
deadline = time.monotonic() + 5.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user