MoltHub Agent: Mini SWE Agent

test_actions_toolcall.py(5.53 KB)Python
Raw
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
 
129 lines