fix(test_concurrent_interrupt): add _tool_guardrails to _Stub fixture (partial close hermes-agent#9)
Some checks failed
Nix / nix (macos-latest) (pull_request) Waiting to run
Supply Chain Audit / Scan PR for critical supply chain risks (pull_request) Successful in 19s
Contributor Attribution Check / check-attribution (pull_request) Failing after 19s
Tests / e2e (pull_request) Successful in 32s
Tests / test (pull_request) Failing after 8m0s
Nix / nix (ubuntu-latest) (pull_request) Failing after 12m25s
Some checks failed
Nix / nix (macos-latest) (pull_request) Waiting to run
Supply Chain Audit / Scan PR for critical supply chain risks (pull_request) Successful in 19s
Contributor Attribution Check / check-attribution (pull_request) Failing after 19s
Tests / e2e (pull_request) Successful in 32s
Tests / test (pull_request) Failing after 8m0s
Nix / nix (ubuntu-latest) (pull_request) Failing after 12m25s
The `_Stub` fixture in tests/run_agent/test_concurrent_interrupt.py
bypasses `AIAgent.__init__`, so it must mirror any new instance attributes
that production methods rely on. Tool-loop guardrails (introduced in
58b89965 "fix(agent): add tool-call loop guardrails", 2026-04-27) added
three integration points to `_execute_tool_calls_concurrent`:
1. `self._tool_guardrails.before_call(...)` per tool (run_agent.py:9447)
2. `self._append_guardrail_observation(...)` per result (run_agent.py:9672)
3. `self._guardrail_block_result(...)` for blocked calls
`_Stub` defined none of these, so both
`test_concurrent_interrupt_cancels_pending` and
`test_running_concurrent_worker_sees_is_interrupted` raised
`AttributeError: '_Stub' object has no attribute '_tool_guardrails'`
on the first concurrent tool call.
Fix:
- Add a real `ToolCallGuardrailController()` instance attribute, matching
AIAgent.__init__ at run_agent.py:1160. Default config is warning-only
so the controller observes but never blocks — the tests still exercise
interrupt fanout, not guardrail behaviour.
- Bind the real `_append_guardrail_observation` and `_guardrail_block_result`
helpers from AIAgent (same pattern as the existing `_execute_tool_calls_concurrent`
/ `interrupt` / `clear_interrupt` bindings).
- Stub `_set_tool_guardrail_halt` as a no-op + add `_tool_guardrail_halt_decision = None`.
- Widen `slow_tool` and `polling_tool` side-effect signatures with `**kwargs`
to swallow new production-only `_invoke_tool` kwargs (`messages`,
`pre_tool_block_checked`).
Verification:
- pytest tests/run_agent/test_concurrent_interrupt.py -v # 4/4 pass
- pytest tests/run_agent/ # 1193 passed,
9 skipped, only pre-existing test_primary_runtime_restore failure
(issue #9 cluster, untouched here).
Diff scope: single file, 21 insertions, 2 modifications.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f8926cc96
commit
b200cba562
@ -20,6 +20,7 @@ def _make_agent(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_INFERENCE_PROVIDER", "")
|
||||
# Avoid full AIAgent init — just import the class and build a stub
|
||||
import run_agent as _ra
|
||||
from agent.tool_guardrails import ToolCallGuardrailController
|
||||
|
||||
class _Stub:
|
||||
_interrupt_requested = False
|
||||
@ -53,6 +54,12 @@ def _make_agent(monkeypatch):
|
||||
self._tool_worker_threads: set = set()
|
||||
self._tool_worker_threads_lock = threading.Lock()
|
||||
self._active_children_lock = threading.Lock()
|
||||
# Mirror AIAgent.__init__ (run_agent.py:1160 — added in 58b89965
|
||||
# "fix(agent): add tool-call loop guardrails", 2026-04-27).
|
||||
# _execute_tool_calls_concurrent calls self._tool_guardrails
|
||||
# .before_call(...) on every tool, so the stub needs a real
|
||||
# controller instance with default (warning-only) config.
|
||||
self._tool_guardrails = ToolCallGuardrailController()
|
||||
|
||||
def _touch_activity(self, desc):
|
||||
self._last_activity = time.time()
|
||||
@ -77,6 +84,14 @@ 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-loop guardrails (added in 58b89965, 2026-04-27) are invoked
|
||||
# before/after every concurrent tool. Bind the real helpers — the
|
||||
# default ToolCallGuardrailController() above is warning-only so
|
||||
# they never block a tool, just observe.
|
||||
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 = lambda *a, **kw: None
|
||||
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 +122,9 @@ 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 swallows production-only kwargs (messages,
|
||||
# pre_tool_block_checked) added to _invoke_tool over time.
|
||||
if name == "slow_one":
|
||||
# Block until the test sets the interrupt
|
||||
barrier.wait(timeout=10)
|
||||
@ -184,7 +201,9 @@ 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 swallows production-only kwargs (pre_tool_block_checked)
|
||||
# added to _invoke_tool over time.
|
||||
observed["worker_tid"] = threading.current_thread().ident
|
||||
worker_started.set()
|
||||
deadline = time.monotonic() + 5.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user