Python · 9133 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_rejects_context_mismatch(temp_dir: Path) -> None:
125 target = temp_dir / "sample.txt"
126 target.write_text("alpha\nbeta\ngamma\n")
127 tool = PatchTool(workspace_root=temp_dir)
128
129 result = await tool.execute(
130 file_path=str(target),
131 hunks=[
132 {
133 "old_start": 2,
134 "old_lines": 1,
135 "new_start": 2,
136 "new_lines": 1,
137 "lines": ["-wrong line", "+beta updated"],
138 }
139 ],
140 )
141
142 assert result.is_error is True
143 assert "context mismatch" in result.output
144
145
146 @pytest.mark.asyncio
147 async def test_patch_tool_accepts_replacement_block_hunks(temp_dir: Path) -> None:
148 target = temp_dir / "sample.txt"
149 target.write_text("alpha\nbeta\ngamma\ndelta\n")
150 tool = PatchTool(workspace_root=temp_dir)
151
152 result = await tool.execute(
153 file_path=str(target),
154 hunks=[
155 {
156 "old_start": 2,
157 "old_end": 3,
158 "new_lines": [
159 "beta updated",
160 "gamma updated",
161 "inserted line",
162 ],
163 }
164 ],
165 )
166
167 assert result.is_error is False
168 assert target.read_text() == "alpha\nbeta updated\ngamma updated\ninserted line\ndelta\n"
169 assert result.metadata["structured_patch"]
170
171
172 @pytest.mark.asyncio
173 async def test_patch_tool_accepts_raw_lines_replacement_hunks(
174 temp_dir: Path,
175 ) -> None:
176 target = temp_dir / "sample.html"
177 target.write_text("<h1>Title</h1>\n<p>Short.</p>\n<footer>Back</footer>\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_lines": 1,
186 "new_start": 2,
187 "new_lines": 4,
188 "lines": [
189 "<p>Expanded body copy.</p>",
190 "",
191 "<ul>",
192 "<li>Concrete detail</li>",
193 "</ul>",
194 ],
195 }
196 ],
197 )
198
199 assert result.is_error is False
200 assert target.read_text() == (
201 "<h1>Title</h1>\n"
202 "<p>Expanded body copy.</p>\n"
203 "\n"
204 "<ul>\n"
205 "<li>Concrete detail</li>\n"
206 "</ul>\n"
207 "<footer>Back</footer>\n"
208 )
209
210
211 @pytest.mark.asyncio
212 async def test_patch_tool_accepts_unified_diff_string(temp_dir: Path) -> None:
213 target = temp_dir / "sample.txt"
214 target.write_text("alpha\nbeta\ngamma\n")
215 tool = PatchTool(workspace_root=temp_dir)
216
217 result = await tool.execute(
218 file_path=str(target),
219 patch=(
220 "--- a/sample.txt\n"
221 "+++ b/sample.txt\n"
222 "@@ -2,1 +2,1 @@\n"
223 "-beta\n"
224 "+beta updated\n"
225 ),
226 )
227
228 assert result.is_error is False
229 assert target.read_text() == "alpha\nbeta updated\ngamma\n"
230 assert result.metadata["structured_patch"]
231
232
233 @pytest.mark.asyncio
234 async def test_patch_tool_rejects_invalid_unified_diff_string(temp_dir: Path) -> None:
235 target = temp_dir / "sample.txt"
236 target.write_text("alpha\nbeta\ngamma\n")
237 tool = PatchTool(workspace_root=temp_dir)
238
239 result = await tool.execute(
240 file_path=str(target),
241 patch="--- a/sample.txt\n+++ b/sample.txt\n@@ ...\n",
242 )
243
244 assert result.is_error is True
245 assert "invalid unified-diff hunk header" in result.output
246
247
248 @pytest.mark.asyncio
249 async def test_git_tool_inspects_read_only_repo_state(temp_dir: Path) -> None:
250 subprocess.run(["git", "init", "--quiet"], cwd=temp_dir, check=True)
251 (temp_dir / "README.md").write_text("loader\n")
252
253 tool = GitTool(workspace_root=temp_dir)
254 status = await tool.execute(action="status", args=["--short"], cwd=".")
255 branch = await tool.execute(action="branch", args=["--show-current"], cwd=".")
256
257 assert status.is_error is False
258 assert "README.md" in status.output
259 assert branch.is_error is False
260
261
262 @pytest.mark.asyncio
263 async def test_notepad_append_alias_updates_selected_section(temp_dir: Path) -> None:
264 registry = create_default_registry(temp_dir)
265
266 working = await registry.execute(
267 "notepad_append",
268 content="Investigate explore runtime.",
269 section="working",
270 )
271 manual = await registry.execute(
272 "notepad_append",
273 content="Keep read-only mode strict.",
274 section="manual",
275 )
276 notepad = await registry.execute("notepad_read", section="all")
277
278 assert working.is_error is False
279 assert manual.is_error is False
280 assert "Investigate explore runtime." in notepad.output
281 assert "Keep read-only mode strict." in notepad.output
282
283
284 @pytest.mark.asyncio
285 async def test_richer_ask_user_question_formats_context_and_options() -> None:
286 tool = AskUserQuestionTool()
287
288 async def answer(question: str, options: list[str] | None) -> str:
289 assert "Fix Strategy" in question
290 assert "Pick the safer repair path." in question
291 assert "Which fix should Loader apply?" in question
292 assert options == [
293 "Minimal patch - touch the parser only",
294 "Broader refactor - clean up parser and tests",
295 ]
296 return "1"
297
298 result = await tool.execute(
299 title="Fix Strategy",
300 context="Pick the safer repair path.",
301 question="Which fix should Loader apply?",
302 options=[
303 {"label": "Minimal patch", "description": "touch the parser only"},
304 {"label": "Broader refactor", "description": "clean up parser and tests"},
305 ],
306 user_response_handler=answer,
307 )
308
309 payload = json.loads(result.output)
310 assert result.is_error is False
311 assert payload["answer"] == "Minimal patch - touch the parser only"