tenseleyflow/loader / a3cc97b

Browse files

Accept literal patch hunks

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a3cc97bd60fcd7614619ae4bc8800ead4682ae44
Parents
7944195
Tree
c4bbd4d

4 changed files

StatusFile+-
M src/loader/tools/fs_safety.py 8 4
M tests/test_bash_operator_surfaces.py 11 0
M tests/test_expanded_tools.py 27 0
M tests/test_safeguard_services.py 24 0
src/loader/tools/fs_safety.pymodified
@@ -2,6 +2,7 @@
22
 
33
 from __future__ import annotations
44
 
5
+import ast
56
 import json
67
 import re
78
 from dataclasses import asdict, dataclass
@@ -88,9 +89,9 @@ def coerce_structured_patch_payload(
8889
 ) -> list[dict[str, object] | StructuredPatchHunk]:
8990
     """Normalize structured patch payloads from native tool callers.
9091
 
91
-    Some local models serialize the `hunks` array as a JSON string even though
92
-    the tool schema asks for an array. Treat that as recoverable instead of
93
-    rejecting an otherwise valid patch call.
92
+    Some local models serialize the `hunks` array as a JSON or Python-literal
93
+    string even though the tool schema asks for an array. Treat that as
94
+    recoverable instead of rejecting an otherwise valid patch call.
9495
     """
9596
 
9697
     if isinstance(value, str):
@@ -99,7 +100,10 @@ def coerce_structured_patch_payload(
99100
         try:
100101
             value = json.loads(value)
101102
         except json.JSONDecodeError:
102
-            return []
103
+            try:
104
+                value = ast.literal_eval(value)
105
+            except (SyntaxError, ValueError):
106
+                return []
103107
 
104108
     if isinstance(value, StructuredPatchHunk):
105109
         return [value]
tests/test_bash_operator_surfaces.pymodified
@@ -288,6 +288,17 @@ def test_build_file_mutation_preview_accepts_json_encoded_hunks() -> None:
288288
     assert preview.structured_patch[0].new_lines == 5
289289
 
290290
 
291
+def test_build_file_mutation_preview_accepts_python_literal_hunks() -> None:
292
+    tool_args = _replacement_block_patch_tool_args()
293
+    tool_args["hunks"] = repr(tool_args["hunks"])
294
+
295
+    preview = build_file_mutation_preview("patch", tool_args=tool_args)
296
+
297
+    assert preview is not None
298
+    assert preview.structured_patch[0].old_start == 42
299
+    assert preview.structured_patch[0].new_lines == 5
300
+
301
+
291302
 def test_cli_print_tool_call_renders_bash_panel_without_truncating(monkeypatch: pytest.MonkeyPatch) -> None:
292303
     console = Console(record=True, width=120)
293304
     monkeypatch.setattr(cli_main_module, "console", console)
tests/test_expanded_tools.pymodified
@@ -65,6 +65,33 @@ async def test_patch_tool_accepts_json_encoded_structured_hunks(
6565
     assert target.read_text() == "alpha\nbeta from json string\ngamma\n"
6666
 
6767
 
68
+@pytest.mark.asyncio
69
+async def test_patch_tool_accepts_python_literal_structured_hunks(
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
+    result = await tool.execute(
77
+        file_path=str(target),
78
+        hunks=repr(
79
+            [
80
+                {
81
+                    "old_start": 2,
82
+                    "old_lines": 1,
83
+                    "new_start": 2,
84
+                    "new_lines": 1,
85
+                    "lines": ["-beta", "+beta from literal string"],
86
+                }
87
+            ]
88
+        ),
89
+    )
90
+
91
+    assert result.is_error is False
92
+    assert target.read_text() == "alpha\nbeta from literal string\ngamma\n"
93
+
94
+
6895
 @pytest.mark.asyncio
6996
 async def test_patch_tool_rejects_context_mismatch(temp_dir: Path) -> None:
7097
     target = temp_dir / "sample.txt"
tests/test_safeguard_services.pymodified
@@ -378,6 +378,30 @@ def test_pre_action_validator_allows_json_encoded_patch_hunks() -> None:
378378
     assert result == ValidationResult(valid=True)
379379
 
380380
 
381
+def test_pre_action_validator_allows_python_literal_patch_hunks() -> None:
382
+    validator = PreActionValidator()
383
+
384
+    result = validator.validate(
385
+        "patch",
386
+        {
387
+            "file_path": "notes.txt",
388
+            "hunks": repr(
389
+                [
390
+                    {
391
+                        "old_start": 1,
392
+                        "old_lines": 1,
393
+                        "new_start": 1,
394
+                        "new_lines": 1,
395
+                        "lines": ["-old", "+new"],
396
+                    }
397
+                ]
398
+            ),
399
+        },
400
+    )
401
+
402
+    assert result == ValidationResult(valid=True)
403
+
404
+
381405
 def test_pre_action_validator_blocks_placeholder_html_write(tmp_path: Path) -> None:
382406
     validator = PreActionValidator()
383407