Python · 7600 bytes Raw Blame History
1 """Tests for tool implementations."""
2
3 import pytest
4
5 from loader.tools import (
6 BashTool,
7 ConfirmationRequired,
8 EditTool,
9 GlobTool,
10 GrepTool,
11 ReadTool,
12 WriteTool,
13 )
14 from loader.tools.base import create_default_registry
15
16
17 class TestReadTool:
18 """Tests for ReadTool."""
19
20 @pytest.fixture
21 def tool(self):
22 return ReadTool()
23
24 @pytest.mark.asyncio
25 async def test_read_file(self, tool, sample_file):
26 result = await tool.execute(file_path=str(sample_file))
27 assert not result.is_error
28 assert "Line 1" in result.output
29 assert "Line 2" in result.output
30
31 @pytest.mark.asyncio
32 async def test_read_nonexistent(self, tool, temp_dir):
33 result = await tool.execute(file_path=str(temp_dir / "nonexistent.txt"))
34 assert result.is_error
35 assert "not found" in result.output.lower()
36
37 @pytest.mark.asyncio
38 async def test_read_with_offset(self, tool, sample_file):
39 result = await tool.execute(file_path=str(sample_file), offset=2, limit=1)
40 assert not result.is_error
41 assert "Line 2" in result.output
42 assert "Line 1" not in result.output
43
44 def test_is_not_destructive(self, tool):
45 assert not tool.is_destructive
46
47
48 class TestWriteTool:
49 """Tests for WriteTool."""
50
51 @pytest.fixture
52 def tool(self):
53 return WriteTool()
54
55 @pytest.mark.asyncio
56 async def test_write_file(self, tool, temp_dir):
57 file_path = temp_dir / "new_file.txt"
58 result = await tool.execute(file_path=str(file_path), content="Hello, world!")
59 assert not result.is_error
60 assert file_path.exists()
61 assert file_path.read_text() == "Hello, world!"
62
63 @pytest.mark.asyncio
64 async def test_write_creates_parents(self, tool, temp_dir):
65 file_path = temp_dir / "subdir" / "deep" / "file.txt"
66 result = await tool.execute(file_path=str(file_path), content="nested")
67 assert not result.is_error
68 assert file_path.exists()
69
70 def test_is_destructive(self, tool):
71 assert tool.is_destructive
72
73 def test_requires_confirmation(self, tool):
74 with pytest.raises(ConfirmationRequired) as exc_info:
75 tool.check_confirmation(
76 skip_confirmation=False,
77 file_path="/tmp/test.txt",
78 content="test",
79 )
80 assert "Write to file" in exc_info.value.message
81
82 def test_skip_confirmation(self, tool):
83 # Should not raise
84 tool.check_confirmation(
85 skip_confirmation=True,
86 file_path="/tmp/test.txt",
87 content="test",
88 )
89
90
91 class TestEditTool:
92 """Tests for EditTool."""
93
94 @pytest.fixture
95 def tool(self):
96 return EditTool()
97
98 @pytest.mark.asyncio
99 async def test_edit_file(self, tool, sample_file):
100 result = await tool.execute(
101 file_path=str(sample_file),
102 old_string="Line 2",
103 new_string="Modified Line 2",
104 )
105 assert not result.is_error
106 assert "Modified Line 2" in sample_file.read_text()
107
108 @pytest.mark.asyncio
109 async def test_edit_nonexistent(self, tool, temp_dir):
110 result = await tool.execute(
111 file_path=str(temp_dir / "nonexistent.txt"),
112 old_string="foo",
113 new_string="bar",
114 )
115 assert result.is_error
116
117 @pytest.mark.asyncio
118 async def test_edit_string_not_found(self, tool, sample_file):
119 result = await tool.execute(
120 file_path=str(sample_file),
121 old_string="Not in file",
122 new_string="replacement",
123 )
124 assert result.is_error
125 assert "not found" in result.output.lower()
126
127
128 class TestGlobTool:
129 """Tests for GlobTool."""
130
131 @pytest.fixture
132 def tool(self):
133 return GlobTool()
134
135 @pytest.mark.asyncio
136 async def test_glob_finds_files(self, tool, temp_dir):
137 (temp_dir / "file1.py").write_text("# python")
138 (temp_dir / "file2.py").write_text("# python")
139 (temp_dir / "file3.txt").write_text("text")
140
141 result = await tool.execute(pattern="*.py", path=str(temp_dir))
142 assert not result.is_error
143 assert "file1.py" in result.output
144 assert "file2.py" in result.output
145 assert "file3.txt" not in result.output
146
147 @pytest.mark.asyncio
148 async def test_glob_no_matches(self, tool, temp_dir):
149 result = await tool.execute(pattern="*.xyz", path=str(temp_dir))
150 assert not result.is_error
151 assert "No files matching" in result.output
152
153
154 class TestBashTool:
155 """Tests for BashTool."""
156
157 @pytest.fixture
158 def tool(self):
159 return BashTool()
160
161 @pytest.mark.asyncio
162 async def test_bash_simple_command(self, tool):
163 result = await tool.execute(command="echo 'hello world'")
164 assert not result.is_error
165 assert "hello world" in result.output
166
167 @pytest.mark.asyncio
168 async def test_bash_pwd(self, tool):
169 result = await tool.execute(command="pwd")
170 assert not result.is_error
171 assert "/" in result.output
172
173 @pytest.mark.asyncio
174 async def test_bash_failed_command(self, tool):
175 result = await tool.execute(command="exit 1")
176 assert result.is_error
177 assert "Exit code 1" in result.output
178
179 def test_is_destructive(self, tool):
180 assert tool.is_destructive
181
182 def test_safe_command_no_confirmation(self, tool):
183 # ls is safe
184 tool.check_confirmation(skip_confirmation=False, command="ls -la")
185 # git status is safe
186 tool.check_confirmation(skip_confirmation=False, command="git status")
187
188 def test_unsafe_command_requires_confirmation(self, tool):
189 with pytest.raises(ConfirmationRequired):
190 tool.check_confirmation(skip_confirmation=False, command="rm -rf /tmp/test")
191
192
193 class TestGrepTool:
194 """Tests for GrepTool."""
195
196 @pytest.fixture
197 def tool(self):
198 return GrepTool()
199
200 @pytest.mark.asyncio
201 async def test_grep_finds_pattern(self, tool, sample_python_file):
202 result = await tool.execute(
203 pattern="def.*hello",
204 path=str(sample_python_file),
205 )
206 assert not result.is_error
207 assert "hello" in result.output
208
209 @pytest.mark.asyncio
210 async def test_grep_no_matches(self, tool, sample_file):
211 result = await tool.execute(
212 pattern="nonexistent_pattern",
213 path=str(sample_file),
214 )
215 assert not result.is_error
216 assert "No matches" in result.output
217
218
219 class TestToolRegistry:
220 """Tests for ToolRegistry."""
221
222 def test_create_default_registry(self):
223 registry = create_default_registry()
224 assert registry.get("read") is not None
225 assert registry.get("write") is not None
226 assert registry.get("edit") is not None
227 assert registry.get("glob") is not None
228 assert registry.get("bash") is not None
229 assert registry.get("grep") is not None
230
231 def test_unknown_tool(self):
232 registry = create_default_registry()
233 assert registry.get("nonexistent") is None
234
235 @pytest.mark.asyncio
236 async def test_execute_unknown_tool(self):
237 registry = create_default_registry()
238 result = await registry.execute("nonexistent")
239 assert result.is_error
240 assert "Unknown tool" in result.output
241
242 def test_skip_confirmation_flag(self):
243 registry = create_default_registry()
244 assert not registry.skip_confirmation
245 registry.skip_confirmation = True
246 assert registry.skip_confirmation