mcp-text-editor
by tumf
- mcp-text-editor
- tests
"""Tests for the TextEditor class."""
import pytest
from mcp_text_editor.text_editor import EditPatch, TextEditor
@pytest.fixture
def editor():
"""Create TextEditor instance."""
return TextEditor()
@pytest.mark.asyncio
async def test_edit_file_with_edit_patch_object(editor, tmp_path):
"""Test editing a file using EditPatch object."""
test_file = tmp_path / "test.txt"
test_file.write_text("line1\nline2\nline3\n")
file_hash = editor.calculate_hash(test_file.read_text())
first_line_content = "line1\n"
# Create an EditPatch object
patch = EditPatch(
start=1,
end=1,
contents="new line\n",
range_hash=editor.calculate_hash(first_line_content),
)
result = await editor.edit_file_contents(str(test_file), file_hash, [patch])
assert result["result"] == "ok"
assert test_file.read_text() == "new line\nline2\nline3\n"
@pytest.fixture
def test_file(tmp_path):
"""Create a temporary test file."""
file_path = tmp_path / "test.txt"
content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
file_path.write_text(content)
return file_path
@pytest.mark.asyncio
async def test_directory_creation_error(editor, tmp_path, mocker):
"""Test file creation when parent directory creation fails."""
test_dir = tmp_path / "test_dir"
test_file = test_dir / "test.txt"
# Mock os.makedirs to raise an OSError
mocker.patch("os.makedirs", side_effect=OSError("Permission denied"))
result = await editor.edit_file_contents(
str(test_file), "", [EditPatch(contents="test content\n", range_hash="")]
)
assert result["result"] == "error"
assert "Failed to create directory" in result["reason"]
assert result["file_hash"] is None
@pytest.mark.asyncio
async def test_missing_range_hash(editor, test_file):
"""Test editing without required range hash."""
_, _, _, file_hash, _, _ = await editor.read_file_contents(test_file)
# Try to edit without range_hash
with pytest.raises(ValueError, match="range_hash is required"):
EditPatch(start=2, end=2, contents="New content\n", range_hash=None)
with pytest.raises(ValueError, match="range_hash is required"):
# Trying with missing range_hash field should also raise
EditPatch(start=2, end=2, contents="New content\n")
@pytest.fixture
def test_invalid_encoding_file(tmp_path):
"""Create a temporary file with a custom encoding to test encoding errors."""
file_path = tmp_path / "invalid_encoding.txt"
# Create Shift-JIS encoded file that will fail to decode with UTF-8
test_data = bytes(
[0x83, 0x65, 0x83, 0x58, 0x83, 0x67, 0x0A]
) # "テスト\n" in Shift-JIS
with open(file_path, "wb") as f:
f.write(test_data)
return str(file_path)
@pytest.mark.asyncio
async def test_calculate_hash(editor):
"""Test hash calculation."""
content = "test content"
hash1 = editor.calculate_hash(content)
hash2 = editor.calculate_hash(content)
assert hash1 == hash2
assert isinstance(hash1, str)
assert len(hash1) == 64 # SHA-256 hash length
@pytest.mark.asyncio
async def test_read_file_contents(editor, test_file):
"""Test reading file contents."""
# Test reading entire file
content, start, end, hash_value, total_lines, size = (
await editor.read_file_contents(str(test_file))
)
assert content == "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
assert start == 1
assert end == 5
assert isinstance(hash_value, str)
assert total_lines == 5
assert size == len(content)
# Test reading specific lines
content, start, end, hash_value, total_lines, size = (
await editor.read_file_contents(str(test_file), start=2, end=4)
)
assert content == "Line 2\nLine 3\nLine 4\n"
assert start == 2
assert end == 4
assert isinstance(hash_value, str)
assert total_lines == 5 # Total lines in file should remain the same
assert size == len(content) # Size should match the selected content
@pytest.mark.asyncio
async def test_encoding_error(editor, test_invalid_encoding_file):
"""Test handling of encoding errors when reading a file with incorrect encoding."""
# Try to read Shift-JIS file with UTF-8 encoding
with pytest.raises(UnicodeDecodeError) as excinfo:
await editor.read_file_contents(test_invalid_encoding_file, encoding="utf-8")
assert "Failed to decode file" in str(excinfo.value)
assert "utf-8" in str(excinfo.value)
# Try to read Shift-JIS file with incorrect encoding in edit_file_contents
result = await editor.edit_file_contents(
test_invalid_encoding_file,
"dummy_hash",
[{"start": 1, "contents": "test", "range_hash": "dummy_hash"}],
encoding="utf-8",
)
assert result["result"] == "error"
assert "Error editing file" in result["reason"]
assert "decode" in result["reason"].lower()
@pytest.mark.asyncio
async def test_create_new_file(editor, tmp_path):
"""Test creating a new file."""
new_file = tmp_path / "new_file.txt"
content = "New file content\nLine 2\n"
# Test creating a new file
result = await editor.edit_file_contents(
str(new_file),
"", # No hash for new file
[
{"start": 1, "contents": content, "range_hash": ""}
], # Empty range_hash for new files
)
assert result["result"] == "ok"
assert new_file.read_text() == content
@pytest.mark.asyncio
async def test_update_file(editor, tmp_path):
"""Test updating an existing file."""
# Create a test file
test_file = tmp_path / "test_update.txt"
original_content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(original_content)
# Read the content and get hash
content, start, end, file_hash, total_lines, size = await editor.read_file_contents(
str(test_file)
)
# Update the second line
new_content = "Updated Line 2\n"
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 2,
"end": 2,
"contents": new_content,
"range_hash": editor.calculate_hash("Line 2\n"),
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == "Line 1\nUpdated Line 2\nLine 3\n"
@pytest.mark.asyncio
async def test_create_file_in_new_directory(editor, tmp_path):
"""Test creating a file in a new directory structure."""
# Test file in a new directory structure
new_file = tmp_path / "subdir" / "nested" / "test.txt"
content = "Content in nested directory\n"
result = await editor.edit_file_contents(
str(new_file),
"", # No hash for new file
[
{"start": 1, "contents": content, "range_hash": ""}
], # Empty range_hash for new file
)
assert result["result"] == "ok"
assert new_file.read_text() == content
@pytest.mark.asyncio
async def test_file_hash_mismatch(editor, tmp_path):
"""Test handling of file hash mismatch."""
# Create a test file
test_file = tmp_path / "test_hash_mismatch.txt"
original_content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(original_content)
result = await editor.edit_file_contents(
str(test_file),
"invalid_hash", # Wrong hash
[
{
"start": 2,
"end": 2,
"contents": "Updated Line\n",
"range_hash": editor.calculate_hash("Line 2\n"),
}
],
)
assert result["result"] == "error"
assert "hash mismatch" in result["reason"].lower()
assert test_file.read_text() == original_content # File should remain unchanged
assert test_file.read_text() == original_content # File should remain unchanged
@pytest.mark.asyncio
async def test_path_traversal_prevention(editor, tmp_path):
"""Test prevention of path traversal attacks."""
test_file = tmp_path / "test.txt"
test_file.write_text("Some content\n")
unsafe_path = str(test_file) + "/.." # Try to traverse up
# Test read operation
with pytest.raises(ValueError) as excinfo:
await editor.read_file_contents(unsafe_path)
assert "Path traversal not allowed" in str(excinfo.value)
# Test write operation
with pytest.raises(ValueError) as excinfo:
await editor.edit_file_contents(
unsafe_path,
"",
[{"start": 1, "contents": "malicious content\n", "range_hash": None}],
)
assert "Path traversal not allowed" in str(excinfo.value)
@pytest.mark.asyncio
async def test_overlapping_patches(editor, tmp_path):
"""Test handling of overlapping patches."""
# Create a test file
test_file = tmp_path / "test_overlap.txt"
original_content = "Line 1\nLine 2\nLine 3\nLine 4\n"
test_file.write_text(original_content)
# Get file hash
content, start, end, file_hash, total_lines, size = await editor.read_file_contents(
str(test_file)
)
# Try to apply overlapping patches
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 1,
"end": 2,
"contents": "New Line 1-2\n",
"range_hash": editor.calculate_hash("Line 1\nLine 2\n"),
},
{
"start": 2,
"end": 3,
"contents": "New Line 2-3\n",
"range_hash": editor.calculate_hash("Line 2\nLine 3\n"),
},
],
)
assert result["result"] == "error"
assert "overlap" in result["reason"].lower()
assert test_file.read_text() == original_content # File should remain unchanged
@pytest.mark.asyncio
async def test_empty_content_handling(editor, tmp_path):
"""Test handling of empty file content."""
# Create an empty test file
test_file = tmp_path / "empty.txt"
test_file.write_text("")
# Read empty file
content, start, end, file_hash, total_lines, size = await editor.read_file_contents(
str(test_file)
)
assert content == ""
assert total_lines == 0
assert size == 0
# Write to empty file (treat it as a new file)
result = await editor.edit_file_contents(
str(test_file),
"", # No hash for empty file
[
{"line_start": 1, "contents": "New content\n", "range_hash": ""}
], # Empty range_hash for new files
)
assert result["result"] == "ok"
assert test_file.read_text() == "New content\n"
@pytest.mark.asyncio
async def test_read_multiple_ranges_line_exceed(editor, tmp_path):
"""Test reading multiple ranges with exceeding line numbers."""
# Create a test file
test_file = tmp_path / "test.txt"
content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(content)
# Request ranges that exceed file length
ranges = [
{
"file_path": str(test_file),
"ranges": [{"start": 4, "end": None}, {"start": 1, "end": 2}],
}
]
result = await editor.read_multiple_ranges(ranges)
# Check the exceeded range
assert len(result[str(test_file)]["ranges"]) == 2
# First range (exceeded)
assert result[str(test_file)]["ranges"][0]["content"] == ""
assert result[str(test_file)]["ranges"][0]["start"] == 4
assert result[str(test_file)]["ranges"][0]["end"] == 4
assert result[str(test_file)]["ranges"][0]["content_size"] == 0
# Second range (normal)
assert result[str(test_file)]["ranges"][1]["content"] == "Line 1\nLine 2\n"
@pytest.mark.asyncio
async def test_directory_creation_failure(editor, tmp_path):
"""Test failure in directory creation."""
# Create a file in place of a directory to cause mkdir to fail
base_dir = tmp_path / "blocked"
base_dir.write_text("")
test_file = base_dir / "subdir" / "test.txt"
result = await editor.edit_file_contents(
str(test_file),
"", # New file
[{"line_start": 1, "contents": "test content\n", "range_hash": None}],
)
assert result["result"] == "error"
assert "Failed to create directory" in result["reason"]
assert result["file_hash"] is None
@pytest.mark.asyncio
async def test_invalid_encoding_file_operations(editor, tmp_path):
"""Test handling of files with invalid encoding during various operations."""
test_file = tmp_path / "invalid_encoding.txt"
# Create a file with Shift-JIS content that will fail UTF-8 decoding
test_data = bytes([0x83, 0x65, 0x83, 0x58, 0x83, 0x67, 0x0A]) # シフトJISのデータ
with open(test_file, "wb") as f:
f.write(test_data)
# Test encoding error in file operations
result = await editor.edit_file_contents(
str(test_file),
"", # hash doesn't matter as it will fail before hash check
[{"line_start": 1, "contents": "new content\n", "range_hash": None}],
encoding="utf-8",
)
assert result["result"] == "error"
assert "Error editing file" in result["reason"]
assert "decode" in result["reason"].lower()
@pytest.mark.asyncio
async def test_create_file_directory_error(editor, tmp_path, monkeypatch):
"""Test creating a file when directory creation fails."""
# Create a path with multiple nested directories
deep_path = tmp_path / "deeply" / "nested" / "path" / "test.txt"
# Mock os.makedirs to raise an OSError
def mock_makedirs(*args, **kwargs):
raise OSError("Permission denied")
monkeypatch.setattr("os.makedirs", mock_makedirs)
# Attempt to create a new file
result = await editor.edit_file_contents(
str(deep_path),
"", # Empty hash for new file
[
{
"start": 1,
"contents": "test content\n",
}
],
)
# Verify proper error handling
assert result["result"] == "error"
assert "Failed to create directory" in result["reason"]
assert "Permission denied" in result["reason"]
assert result["file_hash"] is None
assert "content" not in result
@pytest.mark.asyncio
async def test_create_file_with_empty_directory(editor, tmp_path):
"""Test creating a file when parent directory is an empty string."""
# Create a file in the current directory (no parent directory)
file_path = tmp_path / "test.txt"
# Attempt to create a new file
result = await editor.edit_file_contents(
str(file_path),
"", # Empty hash for new file
[
{
"start": 1,
"contents": "test content\n",
"range_hash": "", # Empty range_hash for new files
}
],
)
# Verify successful file creation
assert result["result"] == "ok"
assert file_path.read_text() == "test content\n"
assert result["file_hash"] is not None
@pytest.mark.asyncio
async def test_file_write_permission_error(editor, tmp_path):
"""Test file write permission error handling."""
# Create a test file
test_file = tmp_path / "readonly.txt"
test_file.write_text("original content\n")
test_file.chmod(0o444) # Make file read-only
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Attempt to modify read-only file
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 1,
"end": 1,
"contents": "new content\n",
"range_hash": editor.calculate_hash("original content\n"),
}
],
)
# Verify proper error handling
assert result["result"] == "error"
assert "Error editing file" in result["reason"]
assert "Permission" in result["reason"]
assert result["file_hash"] is None
assert "content" not in result
@pytest.mark.asyncio
async def test_edit_file_with_none_line_end(editor, tmp_path):
"""Test editing file with end=None."""
test_file = tmp_path / "none_end.txt"
test_file.write_text("line1\nline2\nline3\n")
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Test replacement with None as end
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 2,
"end": 3, # Replace lines 2 and 3
"contents": "new2\nnew3\n",
"range_hash": editor.calculate_hash("line2\nline3\n"),
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == "line1\nnew2\nnew3\n"
@pytest.mark.asyncio
async def test_edit_file_with_exceeding_line_end(editor, tmp_path):
"""Test editing file with end exceeding file length."""
test_file = tmp_path / "exceed_end.txt"
test_file.write_text("line1\nline2\nline3\n")
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Test replacement with end > file length
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 2,
"end": 10, # File only has 3 lines
"contents": "new2\nnew3\n",
"range_hash": editor.calculate_hash("line2\nline3\n"),
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == "line1\nnew2\nnew3\n"
# Clean up
test_file.chmod(0o644) # Restore write permission for cleanup
@pytest.mark.asyncio
async def test_new_file_with_non_empty_hash(editor, tmp_path):
"""Test handling of new file creation with non-empty hash."""
new_file = tmp_path / "nonexistent.txt"
result = await editor.edit_file_contents(
str(new_file),
"non_empty_hash", # Non-empty hash for non-existent file
[{"start": 1, "contents": "test content\n", "range_hash": ""}],
)
# Verify proper error handling
assert result["result"] == "error"
assert result["file_hash"] is None
assert "content" not in result
@pytest.mark.asyncio
async def test_read_file_contents_with_start_beyond_total(editor, tmp_path):
"""Test read_file_contents when start is beyond total lines."""
# Create a test file
test_file = tmp_path / "test.txt"
test_file.write_text("line1\nline2\nline3\n")
# Call read_file_contents with start beyond total lines
content, start, end, content_hash, total_lines, content_size = (
await editor.read_file_contents(str(test_file), start=10)
)
# Verify empty content is returned
assert content == ""
assert start == 9 # start is converted to 0-based indexing
assert end == 9
assert content_hash == editor.calculate_hash("")
assert total_lines == 3
assert content_size == 0
@pytest.mark.asyncio
async def test_create_file_directory_creation_failure(editor, tmp_path, monkeypatch):
"""Test handling of directory creation failure when creating a new file."""
# Create a path with multiple nested directories
deep_path = tmp_path / "deeply" / "nested" / "path" / "test.txt"
# Mock os.makedirs to raise an OSError
def mock_makedirs(*args, **kwargs):
raise OSError("Permission denied")
monkeypatch.setattr("os.makedirs", mock_makedirs)
# Attempt to create a new file
result = await editor.edit_file_contents(
str(deep_path),
"", # Empty hash for new file
[
{
"line_start": 1,
"contents": "test content\n",
}
],
)
# Verify proper error handling
assert result["result"] == "error"
assert "Failed to create directory" in result["reason"]
assert "Permission denied" in result["reason"]
assert result["file_hash"] is None
assert "content" not in result
@pytest.mark.asyncio
async def test_io_error_handling(editor, tmp_path, monkeypatch):
"""Test handling of IO errors during file operations."""
test_file = tmp_path / "test.txt"
content = "test content\n"
test_file.write_text(content)
def mock_open(*args, **kwargs):
raise IOError("Test IO Error")
monkeypatch.setattr("builtins.open", mock_open)
result = await editor.edit_file_contents(
str(test_file),
"",
[{"line_start": 1, "contents": "new content\n"}],
)
assert result["result"] == "error"
assert "Error editing file" in result["reason"]
assert "Test IO Error" in result["reason"]
@pytest.mark.asyncio
async def test_exception_handling(editor, tmp_path, monkeypatch):
"""Test handling of unexpected exceptions during file operations."""
test_file = tmp_path / "test.txt"
def mock_open(*args, **kwargs):
raise Exception("Unexpected error")
monkeypatch.setattr("builtins.open", mock_open)
result = await editor.edit_file_contents(
str(test_file),
"",
[{"line_start": 1, "contents": "new content\n"}],
)
assert result["result"] == "error"
@pytest.mark.asyncio
async def test_insert_operation(editor, tmp_path):
"""Test file insertion operations."""
test_file = tmp_path / "test.txt"
test_file.write_text("line1\nline2\nline3\n")
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Test insertion operation (inserting at line 2)
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 2,
"end": None, # For insertion mode (empty range_hash), end is optional
"contents": "new line\n",
"range_hash": "", # Empty range_hash means insertion mode
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == "line1\nnew line\nline2\nline3\n"
@pytest.mark.asyncio
async def test_content_without_newline(editor, tmp_path):
"""Test handling content without trailing newline."""
test_file = tmp_path / "test.txt"
test_file.write_text("line1\nline2\nline3\n")
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Update with content that doesn't have a trailing newline
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 2,
"end": 2,
"contents": "new line", # No trailing newline
"range_hash": editor.calculate_hash("line2\n"),
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == "line1\nnew line\nline3\n"
result = await editor.edit_file_contents(
str(test_file),
"",
[{"start": 1, "contents": "new content\n"}],
)
assert result["result"] == "error"
assert "Unexpected error" in result["reason"]
@pytest.mark.asyncio
async def test_invalid_line_range(editor, tmp_path):
"""Test handling of invalid line range where end line is less than start line."""
test_file = tmp_path / "test.txt"
test_file.write_text("line1\nline2\nline3\n")
# Try to read with invalid line range
with pytest.raises(ValueError) as excinfo:
await editor.read_file_contents(str(test_file), start=3, end=2)
assert "End line must be greater than or equal to start line" in str(excinfo.value)
# Try to edit with invalid line range
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 3,
"end": 2,
"contents": "new content\n",
"range_hash": editor.calculate_hash("line3\n"),
}
],
)
assert result["result"] == "error"
assert "End line must be greater than or equal to start line" in result["reason"]
@pytest.mark.asyncio
async def test_append_mode(editor, tmp_path):
"""Test appending content when start exceeds total lines."""
# Create a test file
test_file = tmp_path / "test_append.txt"
original_content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(original_content)
# Read the content and get hash
content, start, end, file_hash, total_lines, size = await editor.read_file_contents(
str(test_file)
)
# Attempt to append content with start > total_lines
append_content = "Appended Line\n"
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": total_lines + 1, # Start beyond current line count
"contents": append_content,
"range_hash": "", # Empty range_hash for append mode
}
],
)
assert result["result"] == "ok"
assert test_file.read_text() == original_content + append_content
@pytest.mark.asyncio
async def test_dict_patch_with_defaults(editor: TextEditor, tmp_path):
"""Test dictionary patch with default values."""
test_file = tmp_path / "test.txt"
original_content = "line1\nline2\nline3\n"
test_file.write_text(original_content)
# Get first line content and calculate hashes
first_line_content, _, _, _, _, _ = await editor.read_file_contents(
str(test_file), start=1, end=1
)
file_hash = editor.calculate_hash(original_content)
# Edit using dict patch with missing optional fields
patch = {
"contents": "new line\n", # Add newline to maintain file structure
"start": 1,
"end": 1, # Explicitly specify end
"range_hash": editor.calculate_hash(first_line_content),
}
result = await editor.edit_file_contents(str(test_file), file_hash, [patch])
assert result["result"] == "ok"
# Should replace line 1 when range_hash is provided
assert test_file.read_text() == "new line\nline2\nline3\n"
@pytest.mark.asyncio
async def test_edit_file_without_end(editor, tmp_path):
"""Test editing a file using dictionary patch without end."""
test_file = tmp_path / "test.txt"
content = "line1\nline2\nline3\n"
test_file.write_text(content)
# Create a patch with minimal fields
patch = EditPatch(
contents="new line\n",
start=1,
end=1,
range_hash=editor.calculate_hash("line1\n"),
)
# Calculate file hash from original content
file_hash = editor.calculate_hash(content)
result = await editor.edit_file_contents(str(test_file), file_hash, [patch])
assert result["result"] == "ok"
assert test_file.read_text() == "new line\nline2\nline3\n"
def test_validate_environment():
"""Test environment validation."""
# Currently _validate_environment is a placeholder
# This test ensures the method exists and can be called without errors
TextEditor()._validate_environment()
@pytest.mark.asyncio
async def test_io_error_during_final_write(editor, tmp_path, monkeypatch):
"""Test handling of IO errors during final content writing."""
test_file = tmp_path / "test.txt"
test_file.write_text("original content\n")
# Get file hash
content, _, _, file_hash, _, _ = await editor.read_file_contents(str(test_file))
# Mock open to raise IOError during final write
original_open = open
open_count = 0
def mock_open(*args, **kwargs):
nonlocal open_count
open_count += 1
if open_count > 1: # Allow first open for reading, fail on write
raise IOError("Failed to write file")
return original_open(*args, **kwargs)
monkeypatch.setattr("builtins.open", mock_open)
# Try to edit file with mocked write error
result = await editor.edit_file_contents(
str(test_file),
file_hash,
[
{
"start": 1,
"end": 1,
"contents": "new content\n",
"range_hash": editor.calculate_hash("original content\n"),
}
],
)
assert result["result"] == "error"
assert "Error editing file" in result["reason"]
assert "Failed to write file" in result["reason"]
assert test_file.read_text() == "original content\n" # File should be unchanged
editor._validate_environment()
@pytest.mark.asyncio
async def test_initialization_with_environment_error(monkeypatch):
"""Test TextEditor initialization when environment validation fails."""
def mock_validate_environment(self):
raise EnvironmentError("Failed to validate environment")
# Patch the _validate_environment method
monkeypatch.setattr(TextEditor, "_validate_environment", mock_validate_environment)
# Verify that initialization fails with the expected error
with pytest.raises(EnvironmentError, match="Failed to validate environment"):
TextEditor()
@pytest.mark.asyncio
async def test_read_file_not_found_error(editor, tmp_path):
"""Test FileNotFoundError handling when reading a non-existent file."""
non_existent_file = tmp_path / "does_not_exist.txt"
with pytest.raises(FileNotFoundError) as excinfo:
await editor._read_file(str(non_existent_file))
assert "File not found:" in str(excinfo.value)
assert str(non_existent_file) in str(excinfo.value)