mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-13 10:08:29 +00:00
feat: implement Fast Apply edit_file tool
- Add EditFileTool with Fast Apply approach using full-file rewrites - Implement LLM-powered file editing with context awareness - Add comprehensive unit and integration tests - Include backup functionality for safety - Support multiple file types with proper encoding handling Resolves #3238 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
182
tests/tools/test_edit_file_tool.py
Normal file
182
tests/tools/test_edit_file_tool.py
Normal file
@@ -0,0 +1,182 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.tools.agent_tools.edit_file_tool import EditFileTool, EditFileToolInput
|
||||
|
||||
|
||||
class TestEditFileTool:
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.tool = EditFileTool()
|
||||
|
||||
def test_tool_initialization(self):
|
||||
"""Test that the tool initializes correctly."""
|
||||
assert self.tool.name == "edit_file"
|
||||
assert "Fast Apply" in self.tool.description
|
||||
assert self.tool.args_schema == EditFileToolInput
|
||||
|
||||
def test_input_schema_validation(self):
|
||||
"""Test input schema validation."""
|
||||
valid_input = EditFileToolInput(
|
||||
file_path="/path/to/file.py",
|
||||
edit_instructions="Add a new function",
|
||||
context="This is for testing"
|
||||
)
|
||||
assert valid_input.file_path == "/path/to/file.py"
|
||||
assert valid_input.edit_instructions == "Add a new function"
|
||||
assert valid_input.context == "This is for testing"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
EditFileToolInput(file_path="/path/to/file.py")
|
||||
|
||||
def test_file_not_exists(self):
|
||||
"""Test handling of non-existent files."""
|
||||
result = self.tool._run(
|
||||
file_path="/non/existent/file.py",
|
||||
edit_instructions="Add a function"
|
||||
)
|
||||
assert "Error: File /non/existent/file.py does not exist" in result
|
||||
|
||||
def test_path_is_directory(self):
|
||||
"""Test handling when path points to a directory."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = self.tool._run(
|
||||
file_path=temp_dir,
|
||||
edit_instructions="Add a function"
|
||||
)
|
||||
assert f"Error: {temp_dir} is not a file" in result
|
||||
|
||||
def test_binary_file_handling(self):
|
||||
"""Test handling of binary files."""
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.bin') as temp_file:
|
||||
temp_file.write(b'\x00\x01\x02\x03')
|
||||
temp_file.flush()
|
||||
|
||||
try:
|
||||
result = self.tool._run(
|
||||
file_path=temp_file.name,
|
||||
edit_instructions="Edit this file"
|
||||
)
|
||||
assert "Cannot read" in result and "unsupported encoding" in result
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
@patch('crewai.tools.agent_tools.edit_file_tool.LLM')
|
||||
def test_successful_file_edit(self, mock_llm_class):
|
||||
"""Test successful file editing."""
|
||||
mock_llm = Mock()
|
||||
mock_llm.call.return_value = "def new_function():\n return 'edited'"
|
||||
mock_llm_class.return_value = mock_llm
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as temp_file:
|
||||
temp_file.write("def old_function():\n return 'original'")
|
||||
temp_file.flush()
|
||||
|
||||
try:
|
||||
tool = EditFileTool()
|
||||
result = tool._run(
|
||||
file_path=temp_file.name,
|
||||
edit_instructions="Replace old_function with new_function"
|
||||
)
|
||||
|
||||
assert "Successfully edited" in result
|
||||
assert "Backup saved" in result
|
||||
|
||||
with open(temp_file.name, 'r') as f:
|
||||
content = f.read()
|
||||
assert "new_function" in content
|
||||
assert "old_function" not in content
|
||||
|
||||
backup_path = f"{temp_file.name}.backup"
|
||||
assert os.path.exists(backup_path)
|
||||
with open(backup_path, 'r') as f:
|
||||
backup_content = f.read()
|
||||
assert "old_function" in backup_content
|
||||
|
||||
os.unlink(backup_path)
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
def test_build_fast_apply_prompt(self):
|
||||
"""Test Fast Apply prompt building."""
|
||||
prompt = self.tool._build_fast_apply_prompt(
|
||||
current_content="print('hello')",
|
||||
edit_instructions="Change hello to world",
|
||||
file_path="/test/file.py",
|
||||
context="Testing context"
|
||||
)
|
||||
|
||||
assert "Fast Apply file editing" in prompt
|
||||
assert "print('hello')" in prompt
|
||||
assert "Change hello to world" in prompt
|
||||
assert "/test/file.py" in prompt
|
||||
assert "Testing context" in prompt
|
||||
assert "COMPLETE rewritten file content" in prompt
|
||||
|
||||
def test_extract_file_content_plain(self):
|
||||
"""Test extracting content from plain LLM response."""
|
||||
response = "def hello():\n print('world')"
|
||||
content = self.tool._extract_file_content(response)
|
||||
assert content == "def hello():\n print('world')"
|
||||
|
||||
def test_extract_file_content_markdown(self):
|
||||
"""Test extracting content from markdown code blocks."""
|
||||
response = "```python\ndef hello():\n print('world')\n```"
|
||||
content = self.tool._extract_file_content(response)
|
||||
assert content == "def hello():\n print('world')"
|
||||
|
||||
def test_extract_file_content_empty(self):
|
||||
"""Test handling empty LLM response."""
|
||||
content = self.tool._extract_file_content("")
|
||||
assert content is None
|
||||
|
||||
content = self.tool._extract_file_content("```\n```")
|
||||
assert content == ""
|
||||
|
||||
@patch('crewai.tools.agent_tools.edit_file_tool.LLM')
|
||||
def test_llm_failure_handling(self, mock_llm_class):
|
||||
"""Test handling of LLM failures."""
|
||||
mock_llm = Mock()
|
||||
mock_llm.call.side_effect = Exception("LLM error")
|
||||
mock_llm_class.return_value = mock_llm
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as temp_file:
|
||||
temp_file.write("original content")
|
||||
temp_file.flush()
|
||||
|
||||
try:
|
||||
tool = EditFileTool()
|
||||
result = tool._run(
|
||||
file_path=temp_file.name,
|
||||
edit_instructions="Edit this file"
|
||||
)
|
||||
|
||||
assert "Error editing file" in result
|
||||
assert "LLM error" in result
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
def test_context_parameter(self):
|
||||
"""Test that context parameter is properly handled."""
|
||||
prompt_with_context = self.tool._build_fast_apply_prompt(
|
||||
current_content="test",
|
||||
edit_instructions="edit",
|
||||
file_path="/test.py",
|
||||
context="important context"
|
||||
)
|
||||
|
||||
prompt_without_context = self.tool._build_fast_apply_prompt(
|
||||
current_content="test",
|
||||
edit_instructions="edit",
|
||||
file_path="/test.py",
|
||||
context=None
|
||||
)
|
||||
|
||||
assert "important context" in prompt_with_context
|
||||
assert "ADDITIONAL CONTEXT" in prompt_with_context
|
||||
assert "ADDITIONAL CONTEXT" not in prompt_without_context
|
||||
63
tests/tools/test_edit_file_tool_integration.py
Normal file
63
tests/tools/test_edit_file_tool_integration.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from crewai import Agent
|
||||
from crewai.tools.agent_tools.edit_file_tool import EditFileTool
|
||||
|
||||
|
||||
class TestEditFileToolIntegration:
|
||||
|
||||
def test_agent_with_edit_file_tool(self):
|
||||
"""Test that agents can use the EditFileTool."""
|
||||
tool = EditFileTool()
|
||||
|
||||
agent = Agent(
|
||||
role="Code Editor",
|
||||
goal="Edit files as requested",
|
||||
backstory="I am an expert at editing code files",
|
||||
tools=[tool],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
assert tool in agent.tools
|
||||
assert agent.tools[0].name == "edit_file"
|
||||
|
||||
def test_tool_with_different_file_types(self):
|
||||
"""Test the tool works with different file types."""
|
||||
tool = EditFileTool()
|
||||
|
||||
test_files = [
|
||||
("test.py", "print('hello')", "Change hello to world"),
|
||||
("test.js", "console.log('hello');", "Change hello to world"),
|
||||
("test.txt", "Hello world", "Change Hello to Hi"),
|
||||
("test.md", "# Title\nContent", "Change Title to New Title")
|
||||
]
|
||||
|
||||
for filename, content, instruction in test_files:
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=filename) as temp_file:
|
||||
temp_file.write(content)
|
||||
temp_file.flush()
|
||||
|
||||
try:
|
||||
with patch.object(tool.llm, 'call') as mock_call:
|
||||
expected_content = content.replace('hello', 'world').replace('Hello', 'Hi').replace('Title', 'New Title')
|
||||
mock_call.return_value = expected_content
|
||||
|
||||
result = tool._run(
|
||||
file_path=temp_file.name,
|
||||
edit_instructions=instruction
|
||||
)
|
||||
|
||||
assert "Successfully edited" in result
|
||||
|
||||
with open(temp_file.name, 'r') as f:
|
||||
edited_content = f.read()
|
||||
assert edited_content == expected_content
|
||||
|
||||
backup_path = f"{temp_file.name}.backup"
|
||||
if os.path.exists(backup_path):
|
||||
os.unlink(backup_path)
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
Reference in New Issue
Block a user