From 5f179d6d352dd17b71f7681644fd6cc0cf46eb7c Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Fri, 8 May 2026 03:41:31 +0000 Subject: [PATCH] test(run_agent): keep concurrent-interrupt stub in sync with AIAgent surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tests/run_agent/test_concurrent_interrupt.py | 46 +++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/run_agent/test_concurrent_interrupt.py b/tests/run_agent/test_concurrent_interrupt.py index 9a6ba73e..58be5f07 100644 --- a/tests/run_agent/test_concurrent_interrupt.py +++ b/tests/run_agent/test_concurrent_interrupt.py @@ -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