Repository navigation
Expand file tree
/
Copy pathtest_tool_robustness.py
More file actions
105 lines (84 loc) · 4.45 KB
/
Copy pathtest_tool_robustness.py
File metadata and controls
105 lines (84 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""Regression tests for tool-layer robustness fixes.
Three papercuts observed in a real MetaAgent run, each fixed and pinned here:
1. A model tool call that omits a required parameter (e.g. ``done_tool`` without
``result``) used to raise a raw ``TypeError`` out of the central dispatch and
surface as an opaque "Action failed" — now it returns a clean, recoverable
Response naming the missing parameter.
2. ``bash_tool`` used to mark every non-zero exit code as ``success=False``, so
ordinary diagnostics (``grep -c`` with no match → exit 1, ``ls missing`` →
exit 2) were mislabeled as failed actions. A command that runs to completion is
now a successful observation with the exit code carried in ``data``/message.
"""
import asyncio
from types import SimpleNamespace
import pytest
from autogenesis.config import config
from autogenesis.tool.context import ToolContextManager
from autogenesis.tool.default.bash import BashTool
from autogenesis.tool.default.done import DoneTool
# --------------------------------------------------------------------------- #
# Fix #2 — missing/invalid arguments become a recoverable tool error
# --------------------------------------------------------------------------- #
def _manager_for(tmp_path, instance):
manager = ToolContextManager(base_dir=str(tmp_path))
async def _fake_get_info(name):
return SimpleNamespace(version="1.0.0", instance=instance)
manager.get_info = _fake_get_info
return manager
@pytest.mark.asyncio
async def test_missing_required_arg_returns_clean_error_not_typeerror(tmp_path):
manager = _manager_for(tmp_path, DoneTool())
# done_tool requires both `reasoning` and `result`; omit `result`.
resp = await manager(name="done_tool", input={"reasoning": "all conditions met"})
assert resp.success is False
# The message names the offending tool and the parameter the model forgot,
# rather than leaking "__call__() missing 1 required positional argument".
assert "done_tool" in resp.message
assert "result" in resp.message
assert "positional argument" not in resp.message
@pytest.mark.asyncio
async def test_valid_call_still_dispatches(tmp_path):
manager = _manager_for(tmp_path, DoneTool())
resp = await manager(name="done_tool", input={"reasoning": "r", "result": "ok"})
assert resp.success is True
assert resp.message == "ok"
@pytest.mark.asyncio
async def test_in_body_errors_are_not_masked_by_bind_check(tmp_path):
class _Boom(DoneTool):
async def __call__(self, reasoning: str, result: str, **kwargs):
raise ValueError("boom inside body")
manager = _manager_for(tmp_path, _Boom())
# The bind check must only catch argument-binding errors; a genuine error
# raised inside the tool body must still propagate untouched.
with pytest.raises(ValueError, match="boom inside body"):
await manager(name="done_tool", input={"reasoning": "r", "result": "ok"})
# --------------------------------------------------------------------------- #
# Fix #3 — a bash command that runs is a success, exit code is an observation
# --------------------------------------------------------------------------- #
def test_bash_nonzero_exit_is_successful_observation(tmp_path):
config.workspace_root = str(tmp_path)
tool = BashTool(permission_mode="danger_full_access")
ctx = SimpleNamespace(extra={})
# `grep -c` prints "0" and exits 1 when there are no matches — the canonical
# false-failure case.
resp = asyncio.run(tool(command="echo needle | grep -c missing", ctx=ctx))
assert resp.success is True
assert resp.data["exit_code"] == 1
assert "Exit code: 1" in resp.message
def test_bash_true_failure_still_visible(tmp_path):
config.workspace_root = str(tmp_path)
tool = BashTool(permission_mode="danger_full_access")
ctx = SimpleNamespace(extra={})
resp = asyncio.run(tool(command="ls /no/such/path/here", ctx=ctx))
# The tool call succeeds (it ran), but the failure is fully legible: the
# exit code and stderr are in the response for the model to read.
assert resp.success is True
assert resp.data["exit_code"] != 0
assert "Exit code:" in resp.message
assert "STDERR" in resp.message
def test_bash_empty_command_is_a_tool_error(tmp_path):
config.workspace_root = str(tmp_path)
tool = BashTool(permission_mode="danger_full_access")
ctx = SimpleNamespace(extra={})
resp = asyncio.run(tool(command=" ", ctx=ctx))
assert resp.success is False