| 1 | from unittest.mock import MagicMock
|
| 2 |
|
| 3 | import pytest
|
| 4 |
|
| 5 | from minisweagent.exceptions import FormatError
|
| 6 | from minisweagent.models.utils.actions_toolcall import (
|
| 7 | BASH_TOOL,
|
| 8 | format_toolcall_observation_messages,
|
| 9 | parse_toolcall_actions,
|
| 10 | )
|
| 11 |
|
| 12 |
|
| 13 | class TestParseToolcallActions:
|
| 14 | def test_empty_tool_calls_raises_format_error(self):
|
| 15 | with pytest.raises(FormatError) as exc_info:
|
| 16 | parse_toolcall_actions([], format_error_template="{{ error }}")
|
| 17 | assert "No tool calls found" in exc_info.value.messages[0]["content"]
|
| 18 |
|
| 19 | def test_none_tool_calls_raises_format_error(self):
|
| 20 | with pytest.raises(FormatError) as exc_info:
|
| 21 | parse_toolcall_actions(None, format_error_template="{{ error }}")
|
| 22 | assert "No tool calls found" in exc_info.value.messages[0]["content"]
|
| 23 |
|
| 24 | def test_valid_bash_tool_call(self):
|
| 25 | tool_call = MagicMock()
|
| 26 | tool_call.function.name = "bash"
|
| 27 | tool_call.function.arguments = '{"command": "echo hello"}'
|
| 28 | tool_call.id = "call_123"
|
| 29 | assert parse_toolcall_actions([tool_call], format_error_template="{{ error }}") == [
|
| 30 | {"command": "echo hello", "tool_call_id": "call_123"}
|
| 31 | ]
|
| 32 |
|
| 33 | def test_multiple_valid_tool_calls(self):
|
| 34 | calls = []
|
| 35 | for i in range(3):
|
| 36 | tc = MagicMock()
|
| 37 | tc.function.name = "bash"
|
| 38 | tc.function.arguments = f'{{"command": "cmd{i}"}}'
|
| 39 | tc.id = f"call_{i}"
|
| 40 | calls.append(tc)
|
| 41 | result = parse_toolcall_actions(calls, format_error_template="{{ error }}")
|
| 42 | assert len(result) == 3
|
| 43 | assert result[0] == {"command": "cmd0", "tool_call_id": "call_0"}
|
| 44 | assert result[2] == {"command": "cmd2", "tool_call_id": "call_2"}
|
| 45 |
|
| 46 | def test_unknown_tool_raises_format_error(self):
|
| 47 | tool_call = MagicMock()
|
| 48 | tool_call.function.name = "unknown_tool"
|
| 49 | tool_call.function.arguments = '{"command": "test"}'
|
| 50 | tool_call.id = "call_1"
|
| 51 | with pytest.raises(FormatError) as exc_info:
|
| 52 | parse_toolcall_actions([tool_call], format_error_template="{{ error }}")
|
| 53 | assert "Unknown tool 'unknown_tool'" in exc_info.value.messages[0]["content"]
|
| 54 |
|
| 55 | def test_invalid_json_raises_format_error(self):
|
| 56 | tool_call = MagicMock()
|
| 57 | tool_call.function.name = "bash"
|
| 58 | tool_call.function.arguments = "not valid json"
|
| 59 | tool_call.id = "call_1"
|
| 60 | with pytest.raises(FormatError) as exc_info:
|
| 61 | parse_toolcall_actions([tool_call], format_error_template="{{ error }}")
|
| 62 | assert "Error parsing tool call arguments" in exc_info.value.messages[0]["content"]
|
| 63 |
|
| 64 | def test_missing_command_raises_format_error(self):
|
| 65 | tool_call = MagicMock()
|
| 66 | tool_call.function.name = "bash"
|
| 67 | tool_call.function.arguments = '{"other_arg": "value"}'
|
| 68 | tool_call.id = "call_1"
|
| 69 | with pytest.raises(FormatError) as exc_info:
|
| 70 | parse_toolcall_actions([tool_call], format_error_template="{{ error }}")
|
| 71 | assert "Missing 'command' argument" in exc_info.value.messages[0]["content"]
|
| 72 |
|
| 73 |
|
| 74 | class TestFormatToolcallObservationMessages:
|
| 75 | def test_basic_formatting(self):
|
| 76 | actions = [{"command": "echo test", "tool_call_id": "call_1"}]
|
| 77 | outputs = [{"output": "test output", "returncode": 0}]
|
| 78 | result = format_toolcall_observation_messages(
|
| 79 | actions=actions, outputs=outputs, observation_template="{{ output.output }}"
|
| 80 | )
|
| 81 | assert len(result) == 1
|
| 82 | assert result[0]["role"] == "tool"
|
| 83 | assert result[0]["tool_call_id"] == "call_1"
|
| 84 | assert result[0]["content"] == "test output"
|
| 85 | assert result[0]["extra"]["returncode"] == 0
|
| 86 |
|
| 87 | def test_multiple_outputs(self):
|
| 88 | actions = [
|
| 89 | {"command": "cmd1", "tool_call_id": "call_1"},
|
| 90 | {"command": "cmd2", "tool_call_id": "call_2"},
|
| 91 | ]
|
| 92 | outputs = [{"output": "out1", "returncode": 0}, {"output": "out2", "returncode": 1}]
|
| 93 | result = format_toolcall_observation_messages(
|
| 94 | actions=actions, outputs=outputs, observation_template="{{ output.output }}"
|
| 95 | )
|
| 96 | assert len(result) == 2
|
| 97 | assert result[0]["tool_call_id"] == "call_1"
|
| 98 | assert result[0]["content"] == "out1"
|
| 99 | assert result[1]["tool_call_id"] == "call_2"
|
| 100 | assert result[1]["content"] == "out2"
|
| 101 |
|
| 102 | def test_with_template_vars(self):
|
| 103 | actions = [{"command": "test", "tool_call_id": "call_1"}]
|
| 104 | outputs = [{"output": "result", "returncode": 0}]
|
| 105 | result = format_toolcall_observation_messages(
|
| 106 | actions=actions,
|
| 107 | outputs=outputs,
|
| 108 | observation_template="{{ output.output }} - {{ custom_var }}",
|
| 109 | template_vars={"custom_var": "extra_info"},
|
| 110 | )
|
| 111 | assert result[0]["content"] == "result - extra_info"
|
| 112 |
|
| 113 | def test_exception_info_in_extra(self):
|
| 114 | actions = [{"command": "test", "tool_call_id": "call_1"}]
|
| 115 | outputs = [{"output": "", "returncode": 1, "exception_info": "Error occurred", "extra": {"detail": "more"}}]
|
| 116 | result = format_toolcall_observation_messages(
|
| 117 | actions=actions, outputs=outputs, observation_template="{{ output.output }}"
|
| 118 | )
|
| 119 | assert result[0]["extra"]["exception_info"] == "Error occurred"
|
| 120 | assert result[0]["extra"]["detail"] == "more"
|
| 121 |
|
| 122 |
|
| 123 | class TestBashTool:
|
| 124 | def test_bash_tool_structure(self):
|
| 125 | assert BASH_TOOL["type"] == "function"
|
| 126 | assert BASH_TOOL["function"]["name"] == "bash"
|
| 127 | assert "command" in BASH_TOOL["function"]["parameters"]["properties"]
|
| 128 | assert "command" in BASH_TOOL["function"]["parameters"]["required"]
|
| 129 |
|