hud-python 0.4.45__py3-none-any.whl → 0.5.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"""Tests for apply_patch tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from hud.tools.apply_patch import (
|
|
12
|
+
ActionType,
|
|
13
|
+
ApplyPatchResult,
|
|
14
|
+
ApplyPatchTool,
|
|
15
|
+
Chunk,
|
|
16
|
+
Commit,
|
|
17
|
+
DiffError,
|
|
18
|
+
FileChange,
|
|
19
|
+
Parser,
|
|
20
|
+
Patch,
|
|
21
|
+
PatchAction,
|
|
22
|
+
_apply_commit,
|
|
23
|
+
_find_context,
|
|
24
|
+
_find_context_core,
|
|
25
|
+
_get_updated_file,
|
|
26
|
+
_identify_files_needed,
|
|
27
|
+
_patch_to_commit,
|
|
28
|
+
_text_to_patch,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestApplyPatchResult:
|
|
33
|
+
"""Tests for ApplyPatchResult dataclass."""
|
|
34
|
+
|
|
35
|
+
def test_to_dict_completed(self):
|
|
36
|
+
"""Test to_dict for completed result."""
|
|
37
|
+
result = ApplyPatchResult(status="completed", output="Success")
|
|
38
|
+
assert result.to_dict() == {"status": "completed", "output": "Success"}
|
|
39
|
+
|
|
40
|
+
def test_to_dict_failed(self):
|
|
41
|
+
"""Test to_dict for failed result."""
|
|
42
|
+
result = ApplyPatchResult(status="failed", output="Error message")
|
|
43
|
+
assert result.to_dict() == {"status": "failed", "output": "Error message"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestParser:
|
|
47
|
+
"""Tests for Parser class."""
|
|
48
|
+
|
|
49
|
+
def test_is_done_at_end(self):
|
|
50
|
+
"""Test is_done when at end of lines."""
|
|
51
|
+
parser = Parser(current_files={}, lines=["line1"], index=1)
|
|
52
|
+
assert parser.is_done() is True
|
|
53
|
+
|
|
54
|
+
def test_is_done_with_prefix(self):
|
|
55
|
+
"""Test is_done with matching prefix."""
|
|
56
|
+
parser = Parser(current_files={}, lines=["*** End Patch"], index=0)
|
|
57
|
+
assert parser.is_done(("*** End Patch",)) is True
|
|
58
|
+
|
|
59
|
+
def test_is_done_no_match(self):
|
|
60
|
+
"""Test is_done when prefix doesn't match."""
|
|
61
|
+
parser = Parser(current_files={}, lines=["other line"], index=0)
|
|
62
|
+
assert parser.is_done(("*** End Patch",)) is False
|
|
63
|
+
|
|
64
|
+
def test_startswith(self):
|
|
65
|
+
"""Test startswith method."""
|
|
66
|
+
parser = Parser(current_files={}, lines=["*** Update File: test.txt"], index=0)
|
|
67
|
+
assert parser.startswith("*** Update File:") is True
|
|
68
|
+
assert parser.startswith("*** Delete File:") is False
|
|
69
|
+
|
|
70
|
+
def test_read_str_with_prefix(self):
|
|
71
|
+
"""Test read_str extracts text after prefix."""
|
|
72
|
+
parser = Parser(current_files={}, lines=["*** Update File: test.txt"], index=0)
|
|
73
|
+
result = parser.read_str("*** Update File: ")
|
|
74
|
+
assert result == "test.txt"
|
|
75
|
+
assert parser.index == 1
|
|
76
|
+
|
|
77
|
+
def test_read_str_no_match(self):
|
|
78
|
+
"""Test read_str returns empty when prefix doesn't match."""
|
|
79
|
+
parser = Parser(current_files={}, lines=["other line"], index=0)
|
|
80
|
+
result = parser.read_str("*** Update File: ")
|
|
81
|
+
assert result == ""
|
|
82
|
+
assert parser.index == 0
|
|
83
|
+
|
|
84
|
+
def test_read_str_return_everything(self):
|
|
85
|
+
"""Test read_str with return_everything=True."""
|
|
86
|
+
parser = Parser(current_files={}, lines=["*** Update File: test.txt"], index=0)
|
|
87
|
+
result = parser.read_str("*** Update File: ", return_everything=True)
|
|
88
|
+
assert result == "*** Update File: test.txt"
|
|
89
|
+
|
|
90
|
+
def test_parse_add_file(self):
|
|
91
|
+
"""Test parsing add file action."""
|
|
92
|
+
lines = [
|
|
93
|
+
"*** Begin Patch",
|
|
94
|
+
"*** Add File: new.txt",
|
|
95
|
+
"+line 1",
|
|
96
|
+
"+line 2",
|
|
97
|
+
"*** End Patch",
|
|
98
|
+
]
|
|
99
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
100
|
+
parser.parse()
|
|
101
|
+
|
|
102
|
+
assert "new.txt" in parser.patch.actions
|
|
103
|
+
action = parser.patch.actions["new.txt"]
|
|
104
|
+
assert action.type == ActionType.ADD
|
|
105
|
+
assert action.new_file == "line 1\nline 2"
|
|
106
|
+
|
|
107
|
+
def test_parse_delete_file(self):
|
|
108
|
+
"""Test parsing delete file action."""
|
|
109
|
+
lines = [
|
|
110
|
+
"*** Begin Patch",
|
|
111
|
+
"*** Delete File: old.txt",
|
|
112
|
+
"*** End Patch",
|
|
113
|
+
]
|
|
114
|
+
parser = Parser(current_files={"old.txt": "content"}, lines=lines, index=1)
|
|
115
|
+
parser.parse()
|
|
116
|
+
|
|
117
|
+
assert "old.txt" in parser.patch.actions
|
|
118
|
+
action = parser.patch.actions["old.txt"]
|
|
119
|
+
assert action.type == ActionType.DELETE
|
|
120
|
+
|
|
121
|
+
def test_parse_missing_end_patch(self):
|
|
122
|
+
"""Test that truncated patch (no end marker) raises error."""
|
|
123
|
+
lines = [
|
|
124
|
+
"*** Begin Patch",
|
|
125
|
+
"*** Add File: new.txt",
|
|
126
|
+
"+content",
|
|
127
|
+
]
|
|
128
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
129
|
+
with pytest.raises(DiffError, match="Missing End Patch"):
|
|
130
|
+
parser.parse()
|
|
131
|
+
|
|
132
|
+
def test_parse_truncated_update_file(self):
|
|
133
|
+
"""Test that truncated update file patch raises DiffError, not AssertionError."""
|
|
134
|
+
lines = [
|
|
135
|
+
"*** Begin Patch",
|
|
136
|
+
"*** Update File: test.txt",
|
|
137
|
+
]
|
|
138
|
+
parser = Parser(current_files={"test.txt": "content"}, lines=lines, index=1)
|
|
139
|
+
# Should raise DiffError for unexpected EOF, not AssertionError
|
|
140
|
+
with pytest.raises(DiffError):
|
|
141
|
+
parser.parse()
|
|
142
|
+
|
|
143
|
+
def test_startswith_at_eof(self):
|
|
144
|
+
"""Test that startswith at EOF raises DiffError, not AssertionError."""
|
|
145
|
+
parser = Parser(current_files={}, lines=["line"], index=1) # index past end
|
|
146
|
+
with pytest.raises(DiffError, match="Unexpected end of patch"):
|
|
147
|
+
parser.startswith("test")
|
|
148
|
+
|
|
149
|
+
def test_read_str_at_eof(self):
|
|
150
|
+
"""Test that read_str at EOF returns empty string, not AssertionError."""
|
|
151
|
+
parser = Parser(current_files={}, lines=["line"], index=1) # index past end
|
|
152
|
+
result = parser.read_str("test")
|
|
153
|
+
assert result == ""
|
|
154
|
+
|
|
155
|
+
def test_parse_wrong_end_marker(self):
|
|
156
|
+
"""Test that wrong end marker in add file content raises error."""
|
|
157
|
+
lines = [
|
|
158
|
+
"*** Begin Patch",
|
|
159
|
+
"*** Add File: new.txt",
|
|
160
|
+
"+content",
|
|
161
|
+
"*** Wrong End", # This is inside the add file, so it's an invalid line
|
|
162
|
+
]
|
|
163
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
164
|
+
with pytest.raises(DiffError, match="Invalid Add File Line"):
|
|
165
|
+
parser.parse()
|
|
166
|
+
|
|
167
|
+
def test_parse_duplicate_path_error(self):
|
|
168
|
+
"""Test that duplicate paths raise error."""
|
|
169
|
+
lines = [
|
|
170
|
+
"*** Begin Patch",
|
|
171
|
+
"*** Add File: test.txt",
|
|
172
|
+
"+content",
|
|
173
|
+
"*** Add File: test.txt",
|
|
174
|
+
"+more content",
|
|
175
|
+
"*** End Patch",
|
|
176
|
+
]
|
|
177
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
178
|
+
with pytest.raises(DiffError, match="Duplicate Path"):
|
|
179
|
+
parser.parse()
|
|
180
|
+
|
|
181
|
+
def test_parse_update_missing_file_error(self):
|
|
182
|
+
"""Test that updating missing file raises error."""
|
|
183
|
+
lines = [
|
|
184
|
+
"*** Begin Patch",
|
|
185
|
+
"*** Update File: nonexistent.txt",
|
|
186
|
+
" context",
|
|
187
|
+
"*** End Patch",
|
|
188
|
+
]
|
|
189
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
190
|
+
with pytest.raises(DiffError, match="Missing File"):
|
|
191
|
+
parser.parse()
|
|
192
|
+
|
|
193
|
+
def test_parse_delete_missing_file_error(self):
|
|
194
|
+
"""Test that deleting missing file raises error."""
|
|
195
|
+
lines = [
|
|
196
|
+
"*** Begin Patch",
|
|
197
|
+
"*** Delete File: nonexistent.txt",
|
|
198
|
+
"*** End Patch",
|
|
199
|
+
]
|
|
200
|
+
parser = Parser(current_files={}, lines=lines, index=1)
|
|
201
|
+
with pytest.raises(DiffError, match="Missing File"):
|
|
202
|
+
parser.parse()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestHelperFunctions:
|
|
206
|
+
"""Tests for helper functions."""
|
|
207
|
+
|
|
208
|
+
def test_find_context_core_exact_match(self):
|
|
209
|
+
"""Test _find_context_core with exact match."""
|
|
210
|
+
lines = ["a", "b", "c", "d"]
|
|
211
|
+
context = ["b", "c"]
|
|
212
|
+
index, fuzz = _find_context_core(lines, context, 0)
|
|
213
|
+
assert index == 1
|
|
214
|
+
assert fuzz == 0
|
|
215
|
+
|
|
216
|
+
def test_find_context_core_rstrip_match(self):
|
|
217
|
+
"""Test _find_context_core with rstrip match."""
|
|
218
|
+
lines = ["a", "b ", "c ", "d"]
|
|
219
|
+
context = ["b", "c"]
|
|
220
|
+
index, fuzz = _find_context_core(lines, context, 0)
|
|
221
|
+
assert index == 1
|
|
222
|
+
assert fuzz == 1
|
|
223
|
+
|
|
224
|
+
def test_find_context_core_strip_match(self):
|
|
225
|
+
"""Test _find_context_core with strip match."""
|
|
226
|
+
lines = ["a", " b ", " c ", "d"]
|
|
227
|
+
context = ["b", "c"]
|
|
228
|
+
index, fuzz = _find_context_core(lines, context, 0)
|
|
229
|
+
assert index == 1
|
|
230
|
+
assert fuzz == 100
|
|
231
|
+
|
|
232
|
+
def test_find_context_core_no_match(self):
|
|
233
|
+
"""Test _find_context_core with no match."""
|
|
234
|
+
lines = ["a", "b", "c"]
|
|
235
|
+
context = ["x", "y"]
|
|
236
|
+
index, _ = _find_context_core(lines, context, 0)
|
|
237
|
+
assert index == -1
|
|
238
|
+
|
|
239
|
+
def test_find_context_core_empty_context(self):
|
|
240
|
+
"""Test _find_context_core with empty context."""
|
|
241
|
+
lines = ["a", "b"]
|
|
242
|
+
index, fuzz = _find_context_core(lines, [], 0)
|
|
243
|
+
assert index == 0
|
|
244
|
+
assert fuzz == 0
|
|
245
|
+
|
|
246
|
+
def test_find_context_eof(self):
|
|
247
|
+
"""Test _find_context with EOF flag."""
|
|
248
|
+
lines = ["a", "b", "c", "d"]
|
|
249
|
+
context = ["c", "d"]
|
|
250
|
+
index, fuzz = _find_context(lines, context, 0, eof=True)
|
|
251
|
+
assert index == 2
|
|
252
|
+
assert fuzz == 0
|
|
253
|
+
|
|
254
|
+
def test_identify_files_needed(self):
|
|
255
|
+
"""Test _identify_files_needed."""
|
|
256
|
+
text = """*** Begin Patch
|
|
257
|
+
*** Update File: file1.txt
|
|
258
|
+
context
|
|
259
|
+
*** Delete File: file2.txt
|
|
260
|
+
*** Add File: file3.txt
|
|
261
|
+
+new content
|
|
262
|
+
*** End Patch"""
|
|
263
|
+
files = _identify_files_needed(text)
|
|
264
|
+
assert set(files) == {"file1.txt", "file2.txt"}
|
|
265
|
+
|
|
266
|
+
def test_get_updated_file_simple(self):
|
|
267
|
+
"""Test _get_updated_file with simple update."""
|
|
268
|
+
text = "line1\nline2\nline3"
|
|
269
|
+
action = PatchAction(
|
|
270
|
+
type=ActionType.UPDATE,
|
|
271
|
+
chunks=[
|
|
272
|
+
Chunk(orig_index=1, del_lines=["line2"], ins_lines=["new line2"]),
|
|
273
|
+
],
|
|
274
|
+
)
|
|
275
|
+
result = _get_updated_file(text, action, "test.txt")
|
|
276
|
+
assert result == "line1\nnew line2\nline3"
|
|
277
|
+
|
|
278
|
+
def test_patch_to_commit_add(self):
|
|
279
|
+
"""Test _patch_to_commit with add action."""
|
|
280
|
+
patch = Patch(actions={"new.txt": PatchAction(type=ActionType.ADD, new_file="content")})
|
|
281
|
+
commit = _patch_to_commit(patch, {})
|
|
282
|
+
assert "new.txt" in commit.changes
|
|
283
|
+
assert commit.changes["new.txt"].type == ActionType.ADD
|
|
284
|
+
assert commit.changes["new.txt"].new_content == "content"
|
|
285
|
+
|
|
286
|
+
def test_patch_to_commit_delete(self):
|
|
287
|
+
"""Test _patch_to_commit with delete action."""
|
|
288
|
+
patch = Patch(actions={"old.txt": PatchAction(type=ActionType.DELETE)})
|
|
289
|
+
orig = {"old.txt": "old content"}
|
|
290
|
+
commit = _patch_to_commit(patch, orig)
|
|
291
|
+
assert commit.changes["old.txt"].type == ActionType.DELETE
|
|
292
|
+
assert commit.changes["old.txt"].old_content == "old content"
|
|
293
|
+
|
|
294
|
+
def test_apply_commit(self):
|
|
295
|
+
"""Test _apply_commit function."""
|
|
296
|
+
written = {}
|
|
297
|
+
removed = []
|
|
298
|
+
|
|
299
|
+
def write_fn(path, content):
|
|
300
|
+
written[path] = content
|
|
301
|
+
|
|
302
|
+
def remove_fn(path):
|
|
303
|
+
removed.append(path)
|
|
304
|
+
|
|
305
|
+
commit = Commit(
|
|
306
|
+
changes={
|
|
307
|
+
"new.txt": FileChange(type=ActionType.ADD, new_content="new content"),
|
|
308
|
+
"old.txt": FileChange(type=ActionType.DELETE, old_content="old"),
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
_apply_commit(commit, write_fn, remove_fn)
|
|
312
|
+
|
|
313
|
+
assert written == {"new.txt": "new content"}
|
|
314
|
+
assert removed == ["old.txt"]
|
|
315
|
+
|
|
316
|
+
def test_apply_commit_with_move(self):
|
|
317
|
+
"""Test _apply_commit with move operation."""
|
|
318
|
+
written = {}
|
|
319
|
+
removed = []
|
|
320
|
+
|
|
321
|
+
def write_fn(path, content):
|
|
322
|
+
written[path] = content
|
|
323
|
+
|
|
324
|
+
def remove_fn(path):
|
|
325
|
+
removed.append(path)
|
|
326
|
+
|
|
327
|
+
commit = Commit(
|
|
328
|
+
changes={
|
|
329
|
+
"old.txt": FileChange(
|
|
330
|
+
type=ActionType.UPDATE,
|
|
331
|
+
old_content="old",
|
|
332
|
+
new_content="new",
|
|
333
|
+
move_path="renamed.txt",
|
|
334
|
+
),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
_apply_commit(commit, write_fn, remove_fn)
|
|
338
|
+
|
|
339
|
+
assert written == {"renamed.txt": "new"}
|
|
340
|
+
assert removed == ["old.txt"]
|
|
341
|
+
|
|
342
|
+
def test_text_to_patch_invalid(self):
|
|
343
|
+
"""Test _text_to_patch with invalid patch text."""
|
|
344
|
+
with pytest.raises(DiffError, match="Invalid patch text"):
|
|
345
|
+
_text_to_patch("invalid", {})
|
|
346
|
+
|
|
347
|
+
def test_text_to_patch_valid(self):
|
|
348
|
+
"""Test _text_to_patch with valid patch."""
|
|
349
|
+
text = """*** Begin Patch
|
|
350
|
+
*** Add File: test.txt
|
|
351
|
+
+content
|
|
352
|
+
*** End Patch"""
|
|
353
|
+
patch, fuzz = _text_to_patch(text, {})
|
|
354
|
+
assert "test.txt" in patch.actions
|
|
355
|
+
assert fuzz == 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class TestApplyPatchTool:
|
|
359
|
+
"""Tests for ApplyPatchTool."""
|
|
360
|
+
|
|
361
|
+
def test_init_default(self):
|
|
362
|
+
"""Test default initialization."""
|
|
363
|
+
tool = ApplyPatchTool()
|
|
364
|
+
assert tool.base_path == os.path.abspath(".")
|
|
365
|
+
|
|
366
|
+
def test_init_with_base_path(self):
|
|
367
|
+
"""Test initialization with custom base path."""
|
|
368
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
369
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
370
|
+
assert tool.base_path == os.path.abspath(tmpdir)
|
|
371
|
+
|
|
372
|
+
def test_validate_path_absolute(self):
|
|
373
|
+
"""Test that absolute paths are rejected."""
|
|
374
|
+
tool = ApplyPatchTool()
|
|
375
|
+
with pytest.raises(DiffError, match="Absolute paths are not allowed"):
|
|
376
|
+
tool._validate_path("/absolute/path")
|
|
377
|
+
|
|
378
|
+
def test_validate_path_traversal(self):
|
|
379
|
+
"""Test that path traversal is detected."""
|
|
380
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
381
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
382
|
+
with pytest.raises(DiffError, match="Path traversal detected"):
|
|
383
|
+
tool._validate_path("../outside")
|
|
384
|
+
|
|
385
|
+
def test_validate_path_traversal_sibling_prefix(self):
|
|
386
|
+
"""Test that path traversal via sibling directory with shared prefix is detected.
|
|
387
|
+
|
|
388
|
+
Bug: Path traversal check bypassed via sibling directory prefix.
|
|
389
|
+
|
|
390
|
+
The path traversal check `full_path.startswith(self.base_path)` uses string
|
|
391
|
+
prefix matching, which can be bypassed when sibling directories share a name
|
|
392
|
+
prefix with the base directory. For example, if base_path is /tmp/myapp and
|
|
393
|
+
a user provides path ../myapp_sibling/secret.txt, the resolved full_path
|
|
394
|
+
becomes /tmp/myapp_sibling/secret.txt. The check passes because the string
|
|
395
|
+
/tmp/myapp_sibling/secret.txt starts with /tmp/myapp, allowing access to
|
|
396
|
+
files outside the intended sandbox.
|
|
397
|
+
|
|
398
|
+
The fix is to ensure a path separator follows the base path
|
|
399
|
+
(e.g., full_path.startswith(self.base_path + os.sep)) or use os.path.commonpath.
|
|
400
|
+
"""
|
|
401
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
402
|
+
# Create base directory "myapp" and sibling directory "myapp_sibling"
|
|
403
|
+
base_dir = os.path.join(tmpdir, "myapp")
|
|
404
|
+
sibling_dir = os.path.join(tmpdir, "myapp_sibling")
|
|
405
|
+
os.makedirs(base_dir)
|
|
406
|
+
os.makedirs(sibling_dir)
|
|
407
|
+
|
|
408
|
+
# Create a "secret" file in the sibling directory
|
|
409
|
+
secret_file = os.path.join(sibling_dir, "secret.txt")
|
|
410
|
+
Path(secret_file).write_text("secret content")
|
|
411
|
+
|
|
412
|
+
tool = ApplyPatchTool(base_path=base_dir)
|
|
413
|
+
|
|
414
|
+
# Attempt to access the sibling directory via path traversal
|
|
415
|
+
# This should be detected as path traversal, but the bug allows it
|
|
416
|
+
# because "/tmp/.../myapp_sibling/secret.txt".startswith("/tmp/.../myapp")
|
|
417
|
+
# returns True due to string prefix matching
|
|
418
|
+
with pytest.raises(DiffError, match="Path traversal detected"):
|
|
419
|
+
tool._validate_path("../myapp_sibling/secret.txt")
|
|
420
|
+
|
|
421
|
+
def test_validate_path_valid(self):
|
|
422
|
+
"""Test valid path validation."""
|
|
423
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
424
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
425
|
+
result = tool._validate_path("subdir/file.txt")
|
|
426
|
+
# Normalize path separators for cross-platform compatibility
|
|
427
|
+
expected = os.path.normpath(os.path.join(tmpdir, "subdir/file.txt"))
|
|
428
|
+
assert result == expected
|
|
429
|
+
|
|
430
|
+
@pytest.mark.asyncio
|
|
431
|
+
async def test_call_missing_type(self):
|
|
432
|
+
"""Test call with missing operation type."""
|
|
433
|
+
tool = ApplyPatchTool()
|
|
434
|
+
result = await tool(path="test.txt")
|
|
435
|
+
assert result.status == "failed"
|
|
436
|
+
assert "Missing operation type" in result.output
|
|
437
|
+
|
|
438
|
+
@pytest.mark.asyncio
|
|
439
|
+
async def test_call_missing_path(self):
|
|
440
|
+
"""Test call with missing path."""
|
|
441
|
+
tool = ApplyPatchTool()
|
|
442
|
+
result = await tool(type="create_file")
|
|
443
|
+
assert result.status == "failed"
|
|
444
|
+
assert "Missing file path" in result.output
|
|
445
|
+
|
|
446
|
+
@pytest.mark.asyncio
|
|
447
|
+
async def test_call_unknown_type(self):
|
|
448
|
+
"""Test call with unknown operation type."""
|
|
449
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
450
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
451
|
+
result = await tool(type="unknown_op", path="test.txt")
|
|
452
|
+
assert result.status == "failed"
|
|
453
|
+
assert "Unknown operation type" in result.output
|
|
454
|
+
|
|
455
|
+
@pytest.mark.asyncio
|
|
456
|
+
async def test_create_file_success(self):
|
|
457
|
+
"""Test successful file creation."""
|
|
458
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
459
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
460
|
+
result = await tool(
|
|
461
|
+
type="create_file",
|
|
462
|
+
path="new.txt",
|
|
463
|
+
diff="+line 1\n+line 2",
|
|
464
|
+
)
|
|
465
|
+
assert result.status == "completed"
|
|
466
|
+
assert "Created" in result.output
|
|
467
|
+
|
|
468
|
+
# Verify file was created
|
|
469
|
+
with open(os.path.join(tmpdir, "new.txt")) as f: # noqa: ASYNC230
|
|
470
|
+
content = f.read()
|
|
471
|
+
assert content == "line 1\nline 2"
|
|
472
|
+
|
|
473
|
+
@pytest.mark.asyncio
|
|
474
|
+
async def test_create_file_already_exists(self):
|
|
475
|
+
"""Test creating file that already exists."""
|
|
476
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
477
|
+
# Create existing file
|
|
478
|
+
existing_path = os.path.join(tmpdir, "existing.txt")
|
|
479
|
+
Path(existing_path).write_text("existing content")
|
|
480
|
+
|
|
481
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
482
|
+
result = await tool(
|
|
483
|
+
type="create_file",
|
|
484
|
+
path="existing.txt",
|
|
485
|
+
diff="+new content",
|
|
486
|
+
)
|
|
487
|
+
assert result.status == "failed"
|
|
488
|
+
assert "already exists" in result.output
|
|
489
|
+
|
|
490
|
+
@pytest.mark.asyncio
|
|
491
|
+
async def test_create_file_missing_diff(self):
|
|
492
|
+
"""Test creating file without diff."""
|
|
493
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
494
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
495
|
+
result = await tool(
|
|
496
|
+
type="create_file",
|
|
497
|
+
path="new.txt",
|
|
498
|
+
)
|
|
499
|
+
assert result.status == "failed"
|
|
500
|
+
assert "Missing diff" in result.output
|
|
501
|
+
|
|
502
|
+
@pytest.mark.asyncio
|
|
503
|
+
async def test_delete_file_success(self):
|
|
504
|
+
"""Test successful file deletion."""
|
|
505
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
506
|
+
# Create file to delete
|
|
507
|
+
file_path = os.path.join(tmpdir, "to_delete.txt")
|
|
508
|
+
Path(file_path).write_text("content")
|
|
509
|
+
|
|
510
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
511
|
+
result = await tool(
|
|
512
|
+
type="delete_file",
|
|
513
|
+
path="to_delete.txt",
|
|
514
|
+
)
|
|
515
|
+
assert result.status == "completed"
|
|
516
|
+
assert "Deleted" in result.output
|
|
517
|
+
assert not os.path.exists(file_path)
|
|
518
|
+
|
|
519
|
+
@pytest.mark.asyncio
|
|
520
|
+
async def test_delete_file_not_found(self):
|
|
521
|
+
"""Test deleting non-existent file."""
|
|
522
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
523
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
524
|
+
result = await tool(
|
|
525
|
+
type="delete_file",
|
|
526
|
+
path="nonexistent.txt",
|
|
527
|
+
)
|
|
528
|
+
assert result.status == "failed"
|
|
529
|
+
assert "not found" in result.output
|
|
530
|
+
|
|
531
|
+
@pytest.mark.asyncio
|
|
532
|
+
async def test_update_file_success(self):
|
|
533
|
+
"""Test successful file update."""
|
|
534
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
535
|
+
# Create file to update
|
|
536
|
+
file_path = os.path.join(tmpdir, "test.txt")
|
|
537
|
+
Path(file_path).write_text("line1\nline2\nline3")
|
|
538
|
+
|
|
539
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
540
|
+
result = await tool(
|
|
541
|
+
type="update_file",
|
|
542
|
+
path="test.txt",
|
|
543
|
+
diff=" line1\n-line2\n+new line2\n line3",
|
|
544
|
+
)
|
|
545
|
+
assert result.status == "completed"
|
|
546
|
+
assert "Updated" in result.output
|
|
547
|
+
|
|
548
|
+
# Verify file was updated
|
|
549
|
+
with open(file_path) as f: # noqa: ASYNC230
|
|
550
|
+
content = f.read()
|
|
551
|
+
assert content == "line1\nnew line2\nline3"
|
|
552
|
+
|
|
553
|
+
@pytest.mark.asyncio
|
|
554
|
+
async def test_update_file_not_found(self):
|
|
555
|
+
"""Test updating non-existent file."""
|
|
556
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
557
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
558
|
+
result = await tool(
|
|
559
|
+
type="update_file",
|
|
560
|
+
path="nonexistent.txt",
|
|
561
|
+
diff=" line1\n-line2\n+new line2",
|
|
562
|
+
)
|
|
563
|
+
assert result.status == "failed"
|
|
564
|
+
assert "not found" in result.output
|
|
565
|
+
|
|
566
|
+
@pytest.mark.asyncio
|
|
567
|
+
async def test_update_file_missing_diff(self):
|
|
568
|
+
"""Test updating file without diff."""
|
|
569
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
570
|
+
# Create file
|
|
571
|
+
file_path = os.path.join(tmpdir, "test.txt")
|
|
572
|
+
Path(file_path).write_text("content")
|
|
573
|
+
|
|
574
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
575
|
+
result = await tool(
|
|
576
|
+
type="update_file",
|
|
577
|
+
path="test.txt",
|
|
578
|
+
)
|
|
579
|
+
assert result.status == "failed"
|
|
580
|
+
assert "Missing diff" in result.output
|
|
581
|
+
|
|
582
|
+
@pytest.mark.asyncio
|
|
583
|
+
async def test_create_file_with_subdirectory(self):
|
|
584
|
+
"""Test creating file in subdirectory."""
|
|
585
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
586
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
587
|
+
result = await tool(
|
|
588
|
+
type="create_file",
|
|
589
|
+
path="subdir/nested/file.txt",
|
|
590
|
+
diff="+content",
|
|
591
|
+
)
|
|
592
|
+
assert result.status == "completed"
|
|
593
|
+
|
|
594
|
+
# Verify file was created in subdirectory
|
|
595
|
+
file_path = os.path.join(tmpdir, "subdir/nested/file.txt")
|
|
596
|
+
assert os.path.exists(file_path)
|
|
597
|
+
with open(file_path) as f: # noqa: ASYNC230
|
|
598
|
+
assert f.read() == "content"
|
|
599
|
+
|
|
600
|
+
def test_parse_create_diff(self):
|
|
601
|
+
"""Test _parse_create_diff method."""
|
|
602
|
+
tool = ApplyPatchTool()
|
|
603
|
+
content = tool._parse_create_diff("+line 1\n+line 2\n+line 3")
|
|
604
|
+
assert content == "line 1\nline 2\nline 3"
|
|
605
|
+
|
|
606
|
+
def test_parse_create_diff_with_spaces(self):
|
|
607
|
+
"""Test _parse_create_diff with space-prefixed lines."""
|
|
608
|
+
tool = ApplyPatchTool()
|
|
609
|
+
content = tool._parse_create_diff("+line 1\n context\n+line 3")
|
|
610
|
+
assert content == "line 1\ncontext\nline 3"
|
|
611
|
+
|
|
612
|
+
def test_open_file_not_found(self):
|
|
613
|
+
"""Test _open_file with non-existent file."""
|
|
614
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
615
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
616
|
+
with pytest.raises(DiffError, match="File not found"):
|
|
617
|
+
tool._open_file("nonexistent.txt")
|
|
618
|
+
|
|
619
|
+
def test_write_file(self):
|
|
620
|
+
"""Test _write_file method."""
|
|
621
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
622
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
623
|
+
tool._write_file("test.txt", "content")
|
|
624
|
+
|
|
625
|
+
with open(os.path.join(tmpdir, "test.txt")) as f:
|
|
626
|
+
assert f.read() == "content"
|
|
627
|
+
|
|
628
|
+
def test_remove_file(self):
|
|
629
|
+
"""Test _remove_file method."""
|
|
630
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
631
|
+
# Create file
|
|
632
|
+
file_path = os.path.join(tmpdir, "test.txt")
|
|
633
|
+
Path(file_path).write_text("content")
|
|
634
|
+
|
|
635
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
636
|
+
tool._remove_file("test.txt")
|
|
637
|
+
|
|
638
|
+
assert not os.path.exists(file_path)
|
|
639
|
+
|
|
640
|
+
def test_load_files(self):
|
|
641
|
+
"""Test _load_files method."""
|
|
642
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
643
|
+
# Create files
|
|
644
|
+
Path(os.path.join(tmpdir, "file1.txt")).write_text("content1")
|
|
645
|
+
Path(os.path.join(tmpdir, "file2.txt")).write_text("content2")
|
|
646
|
+
|
|
647
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
648
|
+
files = tool._load_files(["file1.txt", "file2.txt"])
|
|
649
|
+
|
|
650
|
+
assert files == {"file1.txt": "content1", "file2.txt": "content2"}
|
|
651
|
+
|
|
652
|
+
@pytest.mark.asyncio
|
|
653
|
+
async def test_update_with_fuzz(self):
|
|
654
|
+
"""Test update that requires fuzzy matching."""
|
|
655
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
656
|
+
# Create file with trailing whitespace
|
|
657
|
+
file_path = os.path.join(tmpdir, "test.txt")
|
|
658
|
+
Path(file_path).write_text("line1 \nline2\nline3")
|
|
659
|
+
|
|
660
|
+
tool = ApplyPatchTool(base_path=tmpdir)
|
|
661
|
+
result = await tool(
|
|
662
|
+
type="update_file",
|
|
663
|
+
path="test.txt",
|
|
664
|
+
diff=" line1\n-line2\n+new line2\n line3",
|
|
665
|
+
)
|
|
666
|
+
assert result.status == "completed"
|
|
667
|
+
# Fuzz > 0 should be reported
|
|
668
|
+
assert "Updated" in result.output
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class TestDataclasses:
|
|
672
|
+
"""Tests for dataclass structures."""
|
|
673
|
+
|
|
674
|
+
def test_file_change(self):
|
|
675
|
+
"""Test FileChange dataclass."""
|
|
676
|
+
change = FileChange(
|
|
677
|
+
type=ActionType.UPDATE,
|
|
678
|
+
old_content="old",
|
|
679
|
+
new_content="new",
|
|
680
|
+
move_path="moved.txt",
|
|
681
|
+
)
|
|
682
|
+
assert change.type == ActionType.UPDATE
|
|
683
|
+
assert change.old_content == "old"
|
|
684
|
+
assert change.new_content == "new"
|
|
685
|
+
assert change.move_path == "moved.txt"
|
|
686
|
+
|
|
687
|
+
def test_commit(self):
|
|
688
|
+
"""Test Commit dataclass."""
|
|
689
|
+
commit = Commit()
|
|
690
|
+
assert commit.changes == {}
|
|
691
|
+
commit.changes["test.txt"] = FileChange(type=ActionType.ADD, new_content="content")
|
|
692
|
+
assert "test.txt" in commit.changes
|
|
693
|
+
|
|
694
|
+
def test_chunk(self):
|
|
695
|
+
"""Test Chunk dataclass."""
|
|
696
|
+
chunk = Chunk(orig_index=5, del_lines=["old"], ins_lines=["new"])
|
|
697
|
+
assert chunk.orig_index == 5
|
|
698
|
+
assert chunk.del_lines == ["old"]
|
|
699
|
+
assert chunk.ins_lines == ["new"]
|
|
700
|
+
|
|
701
|
+
def test_patch_action(self):
|
|
702
|
+
"""Test PatchAction dataclass."""
|
|
703
|
+
action = PatchAction(type=ActionType.ADD, new_file="content")
|
|
704
|
+
assert action.type == ActionType.ADD
|
|
705
|
+
assert action.new_file == "content"
|
|
706
|
+
assert action.chunks == []
|
|
707
|
+
assert action.move_path is None
|
|
708
|
+
|
|
709
|
+
def test_patch(self):
|
|
710
|
+
"""Test Patch dataclass."""
|
|
711
|
+
patch = Patch()
|
|
712
|
+
assert patch.actions == {}
|
|
713
|
+
|
|
714
|
+
def test_action_type_enum(self):
|
|
715
|
+
"""Test ActionType enum values."""
|
|
716
|
+
assert ActionType.ADD.value == "add"
|
|
717
|
+
assert ActionType.DELETE.value == "delete"
|
|
718
|
+
assert ActionType.UPDATE.value == "update"
|