mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
fix(tenki): honour start_line and end_line in read_file (#5616)
* fix(tenki): honour start_line and end_line in read_file The base Sandbox contract declares read_file(path, start_line, end_line), and the tools layer passes both keyword arguments on every ranged read — including the continuation path that a truncated read names with its start_line marker. TenkiSandbox.read_file accepted only path, so any ranged read through a Tenki sandbox raised TypeError and surfaced as "Unexpected error reading file"; a truncated read could not be continued. Slice the text the way the other providers do (e2b, opensandbox, boxlite): the full text when no range is given, otherwise the selected lines joined with newlines. * fix(tenki): clamp a negative start_line and pin the out-of-range read contract Mirror LocalSandbox.read_file and clamp the start line to at least 1 so a negative start_line cannot wrap around through Python's negative-index slicing. Also extend the ranged-read test with the two boundary cases the tools layer depends on: a start past EOF returns an empty string, and a negative start reads from the first line. * fix(tenki): clamp a negative end_line in read_file Mirror LocalSandbox.read_file for the symmetric range boundary: clamp a negative end_line to zero so Python's negative-index slicing cannot silently drop the last line. Pin the empty result in the existing ranged-read test.
This commit is contained in:
parent
6db7e8091f
commit
43c32ade00
@ -282,13 +282,27 @@ class TenkiSandbox(Sandbox):
|
|||||||
|
|
||||||
# ── file operations ─────────────────────────────────────────────────
|
# ── file operations ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def read_file(self, path: str) -> str:
|
def read_file(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
start_line: int | None = None,
|
||||||
|
end_line: int | None = None,
|
||||||
|
) -> str:
|
||||||
resolved = self._resolve_path(path)
|
resolved = self._resolve_path(path)
|
||||||
try:
|
try:
|
||||||
return self._fs_op(lambda fs: fs.read_text(resolved))
|
content = self._fs_op(lambda fs: fs.read_text(resolved))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("read_file %s failed: %s", resolved, e)
|
logger.error("read_file %s failed: %s", resolved, e)
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
if start_line is None and end_line is None:
|
||||||
|
return content
|
||||||
|
lines = (content or "").splitlines()
|
||||||
|
# Clamp like LocalSandbox.read_file: a negative start would otherwise
|
||||||
|
# wrap around through Python's negative-index slicing instead of
|
||||||
|
# reading from the first line.
|
||||||
|
start = max(start_line or 1, 1)
|
||||||
|
end = max(end_line, 0) if end_line is not None else len(lines)
|
||||||
|
return "\n".join(lines[start - 1 : end])
|
||||||
|
|
||||||
def write_file(self, path: str, content: str, append: bool = False) -> None:
|
def write_file(self, path: str, content: str, append: bool = False) -> None:
|
||||||
self._write_bytes(self._resolve_path(path), content.encode("utf-8"), append=append)
|
self._write_bytes(self._resolve_path(path), content.encode("utf-8"), append=append)
|
||||||
|
|||||||
@ -514,6 +514,23 @@ def test_read_missing_file_returns_error() -> None:
|
|||||||
assert box.read_file("/mnt/user-data/workspace/nope.txt").startswith("Error:")
|
assert box.read_file("/mnt/user-data/workspace/nope.txt").startswith("Error:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_supports_bounded_ranges() -> None:
|
||||||
|
"""The tools layer passes ``start_line``/``end_line`` on every ranged read."""
|
||||||
|
box = TenkiSandbox("sb", _FakeSandbox())
|
||||||
|
box.write_file("/mnt/user-data/workspace/range.txt", "line 1\nline 2\nline 3\nline 4\nline 5")
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt") == "line 1\nline 2\nline 3\nline 4\nline 5"
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=2, end_line=4) == "line 2\nline 3\nline 4"
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=4) == "line 4\nline 5"
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=2) == "line 1\nline 2"
|
||||||
|
# A start past EOF comes back empty rather than raising: the tool layer's
|
||||||
|
# "(start_line exceeds file length)" message and the truncated-read
|
||||||
|
# continuation path both depend on that contract.
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=99) == ""
|
||||||
|
# Negative bounds clamp like LocalSandbox instead of wrapping around.
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", start_line=-1) == ("line 1\nline 2\nline 3\nline 4\nline 5")
|
||||||
|
assert box.read_file("/mnt/user-data/workspace/range.txt", end_line=-1) == ""
|
||||||
|
|
||||||
|
|
||||||
def test_download_missing_file_raises_oserror() -> None:
|
def test_download_missing_file_raises_oserror() -> None:
|
||||||
box = TenkiSandbox("sb", _FakeSandbox())
|
box = TenkiSandbox("sb", _FakeSandbox())
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user