Skip to content

Commit 969c8ad

Browse files
committed
feat(Makefile): add coverage check to all target to ensure test coverage is evaluated
fix(text_editor.py): handle empty file content and improve hash verification logic for better error handling test: add comprehensive tests for creating, updating, and handling files in various scenarios to ensure robustness and correctness
1 parent f982f84 commit 969c8ad

File tree

4 files changed

+496
-29
lines changed

4 files changed

+496
-29
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ typecheck:
2424
# Run all checks required before pushing
2525
check: lint typecheck test
2626
fix: check format
27-
all: format check
27+
all: format check coverage

src/mcp_text_editor/text_editor.py

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -268,23 +268,28 @@ async def edit_file_contents(
268268
await self.read_file_contents(file_path, encoding=encoding)
269269
)
270270

271-
if current_hash != expected_hash:
271+
# Treat empty file as new file
272+
if not current_content:
273+
current_content = ""
274+
current_hash = ""
275+
lines = []
276+
elif current_hash != expected_hash:
272277
return {
273278
"result": "error",
274279
"reason": "Hash mismatch - file has been modified",
275280
"hash": None,
276281
"content": current_content,
277282
}
278-
279-
# Convert content to lines for easier manipulation
280-
lines = current_content.splitlines(keepends=True)
283+
else:
284+
# Convert content to lines for easier manipulation
285+
lines = current_content.splitlines(keepends=True)
281286

282287
# Sort patches from bottom to top to avoid line number shifts
283288
sorted_patches = sorted(
284289
patches,
285290
key=lambda x: (
286291
-(x.get("line_start", 1)),
287-
-(x.get("line_end", x.get("line_start", 1))),
292+
-(x.get("line_end", x.get("line_start", 1)) or float("inf")),
288293
),
289294
)
290295

@@ -314,12 +319,12 @@ async def edit_file_contents(
314319
line_start = patch.get("line_start", 1)
315320
line_end = patch.get("line_end", line_start)
316321
expected_range_hash = patch.get("range_hash")
317-
is_insertion = line_end < line_start
322+
is_insertion = False if line_end is None else line_end < line_start
318323

319-
# Skip range_hash for new files and insertions
320-
if not os.path.exists(file_path) or is_insertion:
324+
# Skip range_hash for new files, empty files and insertions
325+
if not os.path.exists(file_path) or not current_content or is_insertion:
321326
expected_range_hash = self.calculate_hash("")
322-
# For existing files and non-insertions, range_hash is required
327+
# For existing, non-empty files and non-insertions, range_hash is required
323328
elif expected_range_hash is None:
324329
return {
325330
"result": "error",
@@ -331,43 +336,58 @@ async def edit_file_contents(
331336
# Handle insertion or replacement
332337
if is_insertion:
333338
target_content = "" # For insertion, we verify empty content
334-
335-
# Convert to 0-based indexing
336-
line_start -= 1
337-
if not is_insertion:
338-
if line_end is not None:
339-
line_end -= 1
340-
else:
341-
line_end = len(lines) - 1
342-
343-
# Ensure we don't exceed file bounds for replacements
344-
line_end = min(line_end, len(lines) - 1)
339+
else:
340+
# Convert to 0-based indexing for existing content
341+
line_start_zero = line_start - 1
345342

346343
# Calculate target content for hash verification
347-
target_lines = lines[line_start : line_end + 1]
348-
target_content = "".join(target_lines)
344+
if line_start_zero >= len(lines):
345+
target_content = ""
346+
else:
347+
# If line_end is None, we read until the end of the file
348+
if line_end is None:
349+
target_lines = lines[line_start_zero:]
350+
else:
351+
# Adjust to 0-based indexing and make inclusive
352+
line_end_zero = min(line_end - 1, len(lines) - 1)
353+
target_lines = lines[line_start_zero : line_end_zero + 1]
354+
target_content = "".join(target_lines)
349355

350356
# Calculate actual range hash and verify only for non-insertions
351357
actual_range_hash = self.calculate_hash(target_content)
352358
if not is_insertion and actual_range_hash != expected_range_hash:
353359
return {
354360
"result": "error",
355-
"reason": f"Range hash mismatch for lines {line_start + 1}-{line_end + 1}",
361+
"reason": f"Range hash mismatch for lines {line_start}-{line_end if line_end else len(lines)} ({actual_range_hash} != {expected_range_hash})",
356362
"hash": None,
357363
"content": current_content,
358364
}
359365

360-
# Replace lines or insert content
366+
# Convert to 0-based indexing for modification
367+
line_start -= 1
368+
if not is_insertion:
369+
# Handle line_end consistently with hash verification
370+
if line_end is None:
371+
# When line_end is None, replace until the end
372+
line_end = len(lines)
373+
else:
374+
line_end = min(line_end, len(lines))
375+
376+
# Apply the changes
361377
new_content = patch["contents"]
362378
if not new_content.endswith("\n"):
363379
new_content += "\n"
364380
new_lines = new_content.splitlines(keepends=True)
381+
365382
if is_insertion:
366-
# For insertion, we insert at line_start
367383
lines[line_start:line_start] = new_lines
368384
else:
369385
# For replacement, we replace the range
370-
lines[line_start : line_end + 1] = new_lines
386+
lines[line_start:line_end] = new_lines
387+
388+
print(f"patch: {patch}")
389+
print(f"line_end: {line_end}")
390+
print(f"is_insertion: {is_insertion}")
371391

372392
# Write the final content back to file
373393
final_content = "".join(lines)
@@ -398,6 +418,10 @@ async def edit_file_contents(
398418
"content": None,
399419
}
400420
except Exception as e:
421+
import traceback
422+
423+
print(f"Error: {str(e)}")
424+
print(f"Traceback:\n{traceback.format_exc()}")
401425
return {
402426
"result": "error",
403427
"reason": str(e),

0 commit comments

Comments
 (0)