Python · 9899 bytes Raw Blame History
1 """Tests for Sprint 06 tool-surface expansion."""
2
3 from __future__ import annotations
4
5 import json
6 import subprocess
7 from pathlib import Path
8
9 import pytest
10
11 from loader.tools.base import create_default_registry
12 from loader.tools.file_tools import PatchTool
13 from loader.tools.git_tools import GitTool
14 from loader.tools.workflow_tools import AskUserQuestionTool
15
16
17 @pytest.mark.asyncio
18 async def test_patch_tool_applies_structured_hunks(temp_dir: Path) -> None:
19 target = temp_dir / "sample.txt"
20 target.write_text("alpha\nbeta\ngamma\n")
21 tool = PatchTool(workspace_root=temp_dir)
22
23 result = await tool.execute(
24 file_path=str(target),
25 hunks=[
26 {
27 "old_start": 2,
28 "old_lines": 1,
29 "new_start": 2,
30 "new_lines": 1,
31 "lines": ["-beta", "+beta updated"],
32 }
33 ],
34 )
35
36 assert result.is_error is False
37 assert target.read_text() == "alpha\nbeta updated\ngamma\n"
38 assert result.metadata["structured_patch"]
39
40
41 @pytest.mark.asyncio
42 async def test_patch_tool_accepts_json_encoded_structured_hunks(
43 temp_dir: Path,
44 ) -> None:
45 target = temp_dir / "sample.txt"
46 target.write_text("alpha\nbeta\ngamma\n")
47 tool = PatchTool(workspace_root=temp_dir)
48
49 result = await tool.execute(
50 file_path=str(target),
51 hunks=json.dumps(
52 [
53 {
54 "old_start": 2,
55 "old_lines": 1,
56 "new_start": 2,
57 "new_lines": 1,
58 "lines": ["-beta", "+beta from json string"],
59 }
60 ]
61 ),
62 )
63
64 assert result.is_error is False
65 assert target.read_text() == "alpha\nbeta from json string\ngamma\n"
66
67
68 @pytest.mark.asyncio
69 async def test_patch_tool_accepts_json_hunks_missing_outer_close(
70 temp_dir: Path,
71 ) -> None:
72 target = temp_dir / "sample.txt"
73 target.write_text("alpha\nbeta\ngamma\n")
74 tool = PatchTool(workspace_root=temp_dir)
75
76 hunk_payload = json.dumps(
77 [
78 {
79 "old_start": 2,
80 "old_lines": 1,
81 "new_start": 2,
82 "new_lines": 1,
83 "lines": ["-beta", "+beta from repaired json string"],
84 }
85 ]
86 )[:-1]
87 result = await tool.execute(
88 file_path=str(target),
89 hunks=hunk_payload,
90 )
91
92 assert result.is_error is False
93 assert target.read_text() == "alpha\nbeta from repaired json string\ngamma\n"
94
95
96 @pytest.mark.asyncio
97 async def test_patch_tool_accepts_python_literal_structured_hunks(
98 temp_dir: Path,
99 ) -> None:
100 target = temp_dir / "sample.txt"
101 target.write_text("alpha\nbeta\ngamma\n")
102 tool = PatchTool(workspace_root=temp_dir)
103
104 result = await tool.execute(
105 file_path=str(target),
106 hunks=repr(
107 [
108 {
109 "old_start": 2,
110 "old_lines": 1,
111 "new_start": 2,
112 "new_lines": 1,
113 "lines": ["-beta", "+beta from literal string"],
114 }
115 ]
116 ),
117 )
118
119 assert result.is_error is False
120 assert target.read_text() == "alpha\nbeta from literal string\ngamma\n"
121
122
123 @pytest.mark.asyncio
124 async def test_patch_tool_accepts_python_literal_hunks_missing_outer_close(
125 temp_dir: Path,
126 ) -> None:
127 target = temp_dir / "sample.txt"
128 target.write_text("alpha\nbeta\ngamma\n")
129 tool = PatchTool(workspace_root=temp_dir)
130
131 hunk_payload = repr(
132 [
133 {
134 "old_start": 2,
135 "old_lines": 1,
136 "new_start": 2,
137 "new_lines": 1,
138 "lines": ["-beta", "+beta from repaired literal string"],
139 }
140 ]
141 )[:-1]
142 result = await tool.execute(
143 file_path=str(target),
144 hunks=hunk_payload,
145 )
146
147 assert result.is_error is False
148 assert target.read_text() == "alpha\nbeta from repaired literal string\ngamma\n"
149
150
151 @pytest.mark.asyncio
152 async def test_patch_tool_rejects_context_mismatch(temp_dir: Path) -> None:
153 target = temp_dir / "sample.txt"
154 target.write_text("alpha\nbeta\ngamma\n")
155 tool = PatchTool(workspace_root=temp_dir)
156
157 result = await tool.execute(
158 file_path=str(target),
159 hunks=[
160 {
161 "old_start": 2,
162 "old_lines": 1,
163 "new_start": 2,
164 "new_lines": 1,
165 "lines": ["-wrong line", "+beta updated"],
166 }
167 ],
168 )
169
170 assert result.is_error is True
171 assert "context mismatch" in result.output
172
173
174 @pytest.mark.asyncio
175 async def test_patch_tool_accepts_replacement_block_hunks(temp_dir: Path) -> None:
176 target = temp_dir / "sample.txt"
177 target.write_text("alpha\nbeta\ngamma\ndelta\n")
178 tool = PatchTool(workspace_root=temp_dir)
179
180 result = await tool.execute(
181 file_path=str(target),
182 hunks=[
183 {
184 "old_start": 2,
185 "old_end": 3,
186 "new_lines": [
187 "beta updated",
188 "gamma updated",
189 "inserted line",
190 ],
191 }
192 ],
193 )
194
195 assert result.is_error is False
196 assert target.read_text() == "alpha\nbeta updated\ngamma updated\ninserted line\ndelta\n"
197 assert result.metadata["structured_patch"]
198
199
200 @pytest.mark.asyncio
201 async def test_patch_tool_accepts_raw_lines_replacement_hunks(
202 temp_dir: Path,
203 ) -> None:
204 target = temp_dir / "sample.html"
205 target.write_text("<h1>Title</h1>\n<p>Short.</p>\n<footer>Back</footer>\n")
206 tool = PatchTool(workspace_root=temp_dir)
207
208 result = await tool.execute(
209 file_path=str(target),
210 hunks=[
211 {
212 "old_start": 2,
213 "old_lines": 1,
214 "new_start": 2,
215 "new_lines": 4,
216 "lines": [
217 "<p>Expanded body copy.</p>",
218 "",
219 "<ul>",
220 "<li>Concrete detail</li>",
221 "</ul>",
222 ],
223 }
224 ],
225 )
226
227 assert result.is_error is False
228 assert target.read_text() == (
229 "<h1>Title</h1>\n"
230 "<p>Expanded body copy.</p>\n"
231 "\n"
232 "<ul>\n"
233 "<li>Concrete detail</li>\n"
234 "</ul>\n"
235 "<footer>Back</footer>\n"
236 )
237
238
239 @pytest.mark.asyncio
240 async def test_patch_tool_accepts_unified_diff_string(temp_dir: Path) -> None:
241 target = temp_dir / "sample.txt"
242 target.write_text("alpha\nbeta\ngamma\n")
243 tool = PatchTool(workspace_root=temp_dir)
244
245 result = await tool.execute(
246 file_path=str(target),
247 patch=(
248 "--- a/sample.txt\n"
249 "+++ b/sample.txt\n"
250 "@@ -2,1 +2,1 @@\n"
251 "-beta\n"
252 "+beta updated\n"
253 ),
254 )
255
256 assert result.is_error is False
257 assert target.read_text() == "alpha\nbeta updated\ngamma\n"
258 assert result.metadata["structured_patch"]
259
260
261 @pytest.mark.asyncio
262 async def test_patch_tool_rejects_invalid_unified_diff_string(temp_dir: Path) -> None:
263 target = temp_dir / "sample.txt"
264 target.write_text("alpha\nbeta\ngamma\n")
265 tool = PatchTool(workspace_root=temp_dir)
266
267 result = await tool.execute(
268 file_path=str(target),
269 patch="--- a/sample.txt\n+++ b/sample.txt\n@@ ...\n",
270 )
271
272 assert result.is_error is True
273 assert "invalid unified-diff hunk header" in result.output
274
275
276 @pytest.mark.asyncio
277 async def test_git_tool_inspects_read_only_repo_state(temp_dir: Path) -> None:
278 subprocess.run(["git", "init", "--quiet"], cwd=temp_dir, check=True)
279 (temp_dir / "README.md").write_text("loader\n")
280
281 tool = GitTool(workspace_root=temp_dir)
282 status = await tool.execute(action="status", args=["--short"], cwd=".")
283 branch = await tool.execute(action="branch", args=["--show-current"], cwd=".")
284
285 assert status.is_error is False
286 assert "README.md" in status.output
287 assert branch.is_error is False
288
289
290 @pytest.mark.asyncio
291 async def test_notepad_append_alias_updates_selected_section(temp_dir: Path) -> None:
292 registry = create_default_registry(temp_dir)
293
294 working = await registry.execute(
295 "notepad_append",
296 content="Investigate explore runtime.",
297 section="working",
298 )
299 manual = await registry.execute(
300 "notepad_append",
301 content="Keep read-only mode strict.",
302 section="manual",
303 )
304 notepad = await registry.execute("notepad_read", section="all")
305
306 assert working.is_error is False
307 assert manual.is_error is False
308 assert "Investigate explore runtime." in notepad.output
309 assert "Keep read-only mode strict." in notepad.output
310
311
312 @pytest.mark.asyncio
313 async def test_richer_ask_user_question_formats_context_and_options() -> None:
314 tool = AskUserQuestionTool()
315
316 async def answer(question: str, options: list[str] | None) -> str:
317 assert "Fix Strategy" in question
318 assert "Pick the safer repair path." in question
319 assert "Which fix should Loader apply?" in question
320 assert options == [
321 "Minimal patch - touch the parser only",
322 "Broader refactor - clean up parser and tests",
323 ]
324 return "1"
325
326 result = await tool.execute(
327 title="Fix Strategy",
328 context="Pick the safer repair path.",
329 question="Which fix should Loader apply?",
330 options=[
331 {"label": "Minimal patch", "description": "touch the parser only"},
332 {"label": "Broader refactor", "description": "clean up parser and tests"},
333 ],
334 user_response_handler=answer,
335 )
336
337 payload = json.loads(result.output)
338 assert result.is_error is False
339 assert payload["answer"] == "Minimal patch - touch the parser only"