mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(read): correct off-by-one for negative offset with trailing newline
The Read tool documents that a negative offset reads from the end (-N = last N lines). Content is split on newline, so a file ending in a newline produces a trailing empty element and total_file_lines is one too high. Every negative offset was therefore off by one: offset=-1 returned the empty string after the final newline instead of the last line, and -N returned N-1 real lines. Exclude the trailing empty element when computing the start line for negative offsets. Adds regression tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -297,8 +297,15 @@ class Read(BaseTool):
|
|||||||
if offset is not None:
|
if offset is not None:
|
||||||
if offset < 0:
|
if offset < 0:
|
||||||
# Negative offset: read from end
|
# Negative offset: read from end
|
||||||
# -20 means "last 20 lines" → start from (total - 20)
|
# -20 means "last 20 lines" → start from (total - 20).
|
||||||
start_line = max(0, total_file_lines + offset)
|
# A file ending in "\n" produces a trailing empty element
|
||||||
|
# from split('\n'); exclude it so offset=-1 returns the
|
||||||
|
# real last line instead of the empty string after the
|
||||||
|
# final newline (and -N returns N real lines).
|
||||||
|
effective_lines = total_file_lines
|
||||||
|
if all_lines and all_lines[-1] == '':
|
||||||
|
effective_lines -= 1
|
||||||
|
start_line = max(0, effective_lines + offset)
|
||||||
else:
|
else:
|
||||||
# Positive offset: read from start (1-indexed)
|
# Positive offset: read from start (1-indexed)
|
||||||
start_line = max(0, offset - 1) # Convert to 0-indexed
|
start_line = max(0, offset - 1) # Convert to 0-indexed
|
||||||
|
|||||||
58
tests/test_read_offset.py
Normal file
58
tests/test_read_offset.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
"""
|
||||||
|
Regression tests for the Read tool's negative offset ("read from end").
|
||||||
|
|
||||||
|
The tool documents that a negative offset reads from the end (e.g. -20 for the
|
||||||
|
last 20 lines). Because content is split on "\n", a file ending in a newline
|
||||||
|
yields a trailing empty element, so the line count was one too high and every
|
||||||
|
negative offset was off by one: offset=-1 returned the empty string after the
|
||||||
|
final newline instead of the last line, and -N returned N-1 real lines.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
from agent.tools.read.read import Read
|
||||||
|
|
||||||
|
|
||||||
|
class TestReadNegativeOffset(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.work = tempfile.mkdtemp()
|
||||||
|
self.tool = Read({"cwd": self.work})
|
||||||
|
|
||||||
|
def _write(self, name, text):
|
||||||
|
path = os.path.join(self.work, name)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_offset_minus_one_returns_last_line(self):
|
||||||
|
path = self._write("f.txt", "line1\nline2\nline3\n") # trailing newline
|
||||||
|
result = self.tool.execute({"path": path, "offset": -1})
|
||||||
|
self.assertEqual(result.status, "success", result.result)
|
||||||
|
content = result.result["content"]
|
||||||
|
self.assertIn("line3", content) # was "" before the fix
|
||||||
|
self.assertNotIn("line2", content)
|
||||||
|
self.assertNotIn("line1", content)
|
||||||
|
|
||||||
|
def test_offset_minus_two_returns_last_two_lines(self):
|
||||||
|
path = self._write("f.txt", "line1\nline2\nline3\n")
|
||||||
|
result = self.tool.execute({"path": path, "offset": -2})
|
||||||
|
self.assertEqual(result.status, "success", result.result)
|
||||||
|
content = result.result["content"]
|
||||||
|
self.assertIn("line2", content)
|
||||||
|
self.assertIn("line3", content)
|
||||||
|
self.assertNotIn("line1", content)
|
||||||
|
|
||||||
|
def test_file_without_trailing_newline_still_works(self):
|
||||||
|
path = self._write("f.txt", "a\nb\nc") # no trailing newline
|
||||||
|
result = self.tool.execute({"path": path, "offset": -1})
|
||||||
|
self.assertEqual(result.status, "success", result.result)
|
||||||
|
self.assertEqual(result.result["content"].strip(), "c")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user