Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions tests/test_edit_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,173 @@ def test_invalid_commands(self, command, sample_file):
"""Test various invalid commands."""
result = tool_function(command, str(sample_file))
assert "Error" in result

class TestReplaceBlock:
def test_replace_middle_block(self, sample_file):
"""Replace lines 2-3 with new content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=2,
end_line=3,
block_content="new line A\nnew line B"
)
assert f"File {sample_file} updated by replacing lines 2-3" in result
expected_content = "line 1\nnew line A\nnew line B\nline 4\nline 5\n"
assert sample_file.read_text() == expected_content

def test_replace_at_start(self, sample_file):
"""Replace lines 1-2 with new content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=1,
end_line=2,
block_content="replacement for start"
)
assert f"File {sample_file} updated by replacing lines 1-2" in result
expected_content = "replacement for start\nline 3\nline 4\nline 5\n"
assert sample_file.read_text() == expected_content

def test_replace_at_end(self, sample_file):
"""Replace lines 4-5 with new content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=4,
end_line=5,
block_content="replacement for end"
)
assert f"File {sample_file} updated by replacing lines 4-5" in result
expected_content = "line 1\nline 2\nline 3\nreplacement for end\n"
assert sample_file.read_text() == expected_content

def test_replace_entire_file(self, sample_file):
"""Replace all lines (1-5) with new content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=1,
end_line=5,
block_content="all new content"
)
assert f"File {sample_file} updated by replacing lines 1-5" in result
expected_content = "all new content\n"
assert sample_file.read_text() == expected_content

def test_replace_single_line(self, sample_file):
"""Replace line 3 with new content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=3,
end_line=3,
block_content="only line 3 replaced"
)
assert f"File {sample_file} updated by replacing lines 3-3" in result
expected_content = "line 1\nline 2\nonly line 3 replaced\nline 4\nline 5\n"
assert sample_file.read_text() == expected_content

def test_delete_block_by_empty_content(self, sample_file):
"""Delete lines 2-4 by providing empty block_content."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=2,
end_line=4,
block_content=""
)
assert f"File {sample_file} updated by replacing lines 2-4" in result
expected_content = "line 1\nline 5\n"
assert sample_file.read_text() == expected_content

def test_replace_with_fewer_lines(self, sample_file):
"""Replace lines 2-4 (3 lines) with 1 line."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=2,
end_line=4,
block_content="one line to replace three"
)
assert f"File {sample_file} updated by replacing lines 2-4" in result
expected_content = "line 1\none line to replace three\nline 5\n"
assert sample_file.read_text() == expected_content

def test_replace_with_more_lines(self, sample_file):
"""Replace line 3 (1 line) with 3 lines."""
result = tool_function(
"replace_block",
str(sample_file),
start_line=3,
end_line=3,
block_content="line A\nline B\nline C"
)
assert f"File {sample_file} updated by replacing lines 3-3" in result
expected_content = "line 1\nline 2\nline A\nline B\nline C\nline 4\nline 5\n"
assert sample_file.read_text() == expected_content

def test_replace_on_empty_file(self, temp_dir):
"""Test replace_block on an empty file."""
empty_file = temp_dir / "empty.txt"
empty_file.write_text("")

result = tool_function(
"replace_block",
str(empty_file),
start_line=1,
end_line=1,
block_content="content for empty file"
)
assert f"File {empty_file} updated by replacing lines 1-1" in result
expected_content = "content for empty file\n"
assert empty_file.read_text() == expected_content

def test_invalid_line_numbers_non_positive(self, sample_file):
"""Test with non-positive line numbers."""
result = tool_function("replace_block", str(sample_file), start_line=0, end_line=1, block_content="fail")
assert "start_line and end_line must be positive integers" in result
result = tool_function("replace_block", str(sample_file), start_line=1, end_line=0, block_content="fail")
assert "start_line and end_line must be positive integers" in result

def test_invalid_line_numbers_start_gt_end(self, sample_file):
"""Test with start_line > end_line."""
result = tool_function("replace_block", str(sample_file), start_line=3, end_line=2, block_content="fail")
assert "start_line (3) cannot be greater than end_line (2)" in result

def test_invalid_line_numbers_out_of_bounds_non_empty_file(self, sample_file):
"""Test with line numbers out of file bounds for a non-empty file."""
result = tool_function("replace_block", str(sample_file), start_line=6, end_line=6, block_content="fail")
assert "start_line (6) is out of bounds for file with 5 lines" in result

result = tool_function("replace_block", str(sample_file), start_line=1, end_line=6, block_content="fail")
assert "end_line (6) is out of bounds for file with 5 lines" in result

def test_invalid_line_numbers_for_empty_file(self, temp_dir):
"""Test invalid line numbers for an empty file."""
empty_file = temp_dir / "empty.txt"
empty_file.write_text("")
result = tool_function("replace_block", str(empty_file), start_line=2, end_line=2, block_content="fail")
assert "For an empty file, start_line and end_line must both be 1" in result

def test_missing_parameters_for_replace_block(self, sample_file):
"""Test with missing required parameters for replace_block."""
result = tool_function("replace_block", str(sample_file), start_line=1, end_line=1) # Missing block_content
assert "Missing or invalid type for required arguments" in result
result = tool_function("replace_block", str(sample_file), end_line=1, block_content="fail") # Missing start_line
assert "Missing or invalid type for required arguments" in result
result = tool_function("replace_block", str(sample_file), start_line=1, block_content="fail") # Missing end_line
assert "Missing or invalid type for required arguments" in result

def test_replace_block_nonexistent_file(self, temp_dir):
"""Test replace_block on a nonexistent file."""
non_existent_file = temp_dir / "does_not_exist.txt"
result = tool_function(
"replace_block",
str(non_existent_file),
start_line=1,
end_line=1,
block_content="content"
)
assert "Error" in result
assert "does not exist" in result
81 changes: 72 additions & 9 deletions tools/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,34 @@ def tool_info():
* The `create` command cannot be used if the specified `path` already exists as a file.\n
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`.\n
* The `edit` command overwrites the entire file with the provided `file_text`.\n
* No partial/line-range edits or partial viewing are supported.""",
* The `replace_block` command replaces a specified range of lines (from `start_line` to `end_line`, inclusive) with `block_content`. If `block_content` is an empty string, the specified lines are deleted. Newlines in `block_content` will result in multiple lines; content is typically joined by newlines to form the final text.\n
* No partial/line-range edits or partial viewing are supported for the basic `edit` or `view` commands (use `replace_block` for precise edits).""",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"enum": ["view", "create", "edit"],
"description": "The command to run: `view`, `create`, or `edit`."
"enum": ["view", "create", "edit", "replace_block"],
"description": "The command to run: `view`, `create`, `edit`, or `replace_block`."
},
"path": {
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
"type": "string"
},
"file_text": {
"description": "Required parameter of `create` or `edit` command, containing the content for the entire file.",
"description": "Required parameter of `create` or `edit` (full file overwrite) command, containing the content for the entire file.",
"type": "string"
},
"start_line": {
"description": "For `replace_block` command: The first line (inclusive, 1-indexed) of the block to be replaced. Must be a positive integer.",
"type": "integer"
},
"end_line": {
"description": "For `replace_block` command: The last line (inclusive, 1-indexed) of the block to be replaced. Must be a positive integer and greater than or equal to `start_line`.",
"type": "integer"
},
"block_content": {
"description": "For `replace_block` command: The new textual content to replace the specified block. Newlines (e.g., '\\n') will create multiple lines in the output. An empty string means the block will be deleted.",
"type": "string"
}
},
Expand All @@ -44,6 +57,7 @@ def validate_path(path: str, command: str) -> Path:
- 'view': path may be a file or directory; must exist.
- 'create': path must not exist (for new file creation).
- 'edit': path must exist (for overwriting).
- 'replace_block': path must exist and be a file (for replacing content).
"""
path_obj = Path(path)

Expand All @@ -61,7 +75,7 @@ def validate_path(path: str, command: str) -> Path:
# Path must not exist
if path_obj.exists():
raise ValueError(f"Cannot create new file; {path} already exists.")
elif command == "edit":
elif command == "edit" or command == "replace_block":
# Path must exist and must be a file
if not path_obj.exists():
raise ValueError(f"The file {path} does not exist.")
Expand Down Expand Up @@ -119,12 +133,14 @@ def view_path(path_obj: Path) -> str:
content = read_file(path_obj)
return format_output(content, str(path_obj))

def tool_function(command: str, path: str, file_text: str = None) -> str:
def tool_function(command: str, path: str, file_text: str = None, **kwargs) -> str:
"""
Main tool function that handles:
- 'view' : View the entire file or directory listing
- 'create': Create a new file with the given file_text
- 'edit' : Overwrite an existing file with file_text
- 'replace_block': Replaces a specific block of lines (inclusive) with new content.
Requires kwargs: start_line (int), end_line (int), block_content (str).
"""
try:
path_obj = validate_path(path, command)
Expand All @@ -144,13 +160,60 @@ def tool_function(command: str, path: str, file_text: str = None) -> str:
write_file(path_obj, file_text)
return f"File at {path} has been overwritten with new content."

elif command == "replace_block":
start_line = kwargs.get("start_line")
end_line = kwargs.get("end_line")
block_content = kwargs.get("block_content")

if not (isinstance(start_line, int) and isinstance(end_line, int) and isinstance(block_content, str)):
raise ValueError("Missing or invalid type for required arguments for 'replace_block': start_line (int), end_line (int), block_content (str).")

if start_line <= 0 or end_line <= 0:
raise ValueError("start_line and end_line must be positive integers for 'replace_block'.")
if start_line > end_line:
raise ValueError(f"start_line ({start_line}) cannot be greater than end_line ({end_line}) for 'replace_block'.")

original_content_str = read_file(path_obj)
original_lines = original_content_str.splitlines()
total_lines = len(original_lines)

new_content_str = ""

if total_lines == 0: # Handling for empty file
if start_line == 1 and end_line == 1:
new_content_str = block_content
else:
raise ValueError(f"For an empty file, start_line and end_line must both be 1 to replace its content. Got start_line={start_line}, end_line={end_line}.")
else: # Handling for non-empty file
if not (1 <= start_line <= total_lines):
raise ValueError(f"start_line ({start_line}) is out of bounds for file with {total_lines} lines.")
if not (start_line <= end_line <= total_lines):
raise ValueError(f"end_line ({end_line}) is out of bounds for file with {total_lines} lines or less than start_line ({start_line}).")

new_content_list = []
new_content_list.extend(original_lines[0:start_line-1]) # Lines before the block

if block_content: # Add new block content if it's not empty
new_content_list.extend(block_content.splitlines())

new_content_list.extend(original_lines[end_line:]) # Lines after the block

new_content_str = "\n".join(new_content_list)

# Ensure a final newline if the content is not empty and doesn't already have one
if new_content_str and not new_content_str.endswith('\n'):
new_content_str += '\n'

# If new_content_str is empty (e.g., all lines deleted and block_content was empty),
# write_file will correctly create an empty file.

write_file(path_obj, new_content_str)
return f"File {path} updated by replacing lines {start_line}-{end_line}."
else:
raise ValueError(f"Unknown command: {command}")

except Exception as e:
return f"Error: {str(e)}"

if __name__ == "__main__":
# Example usage
result = tool_function("view", "./coding_agent.py", view_range=[1, 10])
print(result)
pass