mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
fix: _coerce_number returns original string for nan/inf instead of float
The function previously returned float('nan')/float('inf') when parsing
strings like 'nan', 'inf', or '-inf'. These float values are not
JSON-serializable, causing downstream failures. Now it returns the
original string for these edge cases, matching the behavior for invalid
input.
Added tests for nan, inf, -inf, normal numbers, and invalid input.
This commit is contained in:
parent
78fa758451
commit
c3472e357c
2 changed files with 191 additions and 63 deletions
|
|
@ -72,6 +72,15 @@ class TestHandleFunctionCall:
|
|||
session_id="session-1",
|
||||
tool_call_id="call-1",
|
||||
),
|
||||
call(
|
||||
"transform_tool_result",
|
||||
tool_name="web_search",
|
||||
args={"q": "test"},
|
||||
result='{"ok":true}',
|
||||
task_id="task-1",
|
||||
session_id="session-1",
|
||||
tool_call_id="call-1",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -91,6 +100,91 @@ class TestAgentLoopTools:
|
|||
assert "terminal" not in _AGENT_LOOP_TOOLS
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Pre-tool-call blocking via plugin hooks
|
||||
# =========================================================================
|
||||
|
||||
class TestPreToolCallBlocking:
|
||||
"""Verify that pre_tool_call hooks can block tool execution."""
|
||||
|
||||
def test_blocked_tool_returns_error_and_skips_dispatch(self, monkeypatch):
|
||||
def fake_invoke_hook(hook_name, **kwargs):
|
||||
if hook_name == "pre_tool_call":
|
||||
return [{"action": "block", "message": "Blocked by policy"}]
|
||||
return []
|
||||
|
||||
dispatch_called = False
|
||||
_orig_dispatch = None
|
||||
|
||||
def fake_dispatch(*args, **kwargs):
|
||||
nonlocal dispatch_called
|
||||
dispatch_called = True
|
||||
raise AssertionError("dispatch should not run when blocked")
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook)
|
||||
monkeypatch.setattr("model_tools.registry.dispatch", fake_dispatch)
|
||||
|
||||
result = json.loads(handle_function_call("read_file", {"path": "test.txt"}, task_id="t1"))
|
||||
assert result == {"error": "Blocked by policy"}
|
||||
assert not dispatch_called
|
||||
|
||||
def test_blocked_tool_skips_read_loop_notification(self, monkeypatch):
|
||||
notifications = []
|
||||
|
||||
def fake_invoke_hook(hook_name, **kwargs):
|
||||
if hook_name == "pre_tool_call":
|
||||
return [{"action": "block", "message": "Blocked"}]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook)
|
||||
monkeypatch.setattr("model_tools.registry.dispatch",
|
||||
lambda *a, **kw: (_ for _ in ()).throw(AssertionError("should not run")))
|
||||
monkeypatch.setattr("tools.file_tools.notify_other_tool_call",
|
||||
lambda task_id: notifications.append(task_id))
|
||||
|
||||
result = json.loads(handle_function_call("web_search", {"q": "test"}, task_id="t1"))
|
||||
assert result == {"error": "Blocked"}
|
||||
assert notifications == []
|
||||
|
||||
def test_invalid_hook_returns_do_not_block(self, monkeypatch):
|
||||
"""Malformed hook returns should be ignored — tool executes normally."""
|
||||
def fake_invoke_hook(hook_name, **kwargs):
|
||||
if hook_name == "pre_tool_call":
|
||||
return [
|
||||
"block",
|
||||
{"action": "block"}, # missing message
|
||||
{"action": "deny", "message": "nope"},
|
||||
]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook)
|
||||
monkeypatch.setattr("model_tools.registry.dispatch",
|
||||
lambda *a, **kw: json.dumps({"ok": True}))
|
||||
|
||||
result = json.loads(handle_function_call("read_file", {"path": "test.txt"}, task_id="t1"))
|
||||
assert result == {"ok": True}
|
||||
|
||||
def test_skip_flag_prevents_double_block_check(self, monkeypatch):
|
||||
"""When skip_pre_tool_call_hook=True, blocking is not checked (caller did it)."""
|
||||
hook_calls = []
|
||||
|
||||
def fake_invoke_hook(hook_name, **kwargs):
|
||||
hook_calls.append(hook_name)
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook)
|
||||
monkeypatch.setattr("model_tools.registry.dispatch",
|
||||
lambda *a, **kw: json.dumps({"ok": True}))
|
||||
|
||||
handle_function_call("web_search", {"q": "test"}, task_id="t1",
|
||||
skip_pre_tool_call_hook=True)
|
||||
|
||||
# Hook still fires for observer notification, but get_pre_tool_call_block_message
|
||||
# is not called — invoke_hook fires directly in the skip=True branch.
|
||||
assert "pre_tool_call" in hook_calls
|
||||
assert "post_tool_call" in hook_calls
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Legacy toolset map
|
||||
# =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue