From 89cc6a2463a6a9b28f949e0738059968a7f30698 Mon Sep 17 00:00:00 2001 From: Mu-ty831 <3860902720@qq.com> Date: Sat, 17 Oct 2026 11:06:00 +0800 Subject: [PATCH 1/2] fix(tools): allow whole-file reads of empty files --- .../adk/tools/environment/_read_file_tool.py | 2 ++ .../tools/environment/test_read_file_tool.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/google/adk/tools/environment/_read_file_tool.py b/src/google/adk/tools/environment/_read_file_tool.py index d49dd0d054a..a7c3e9e6a79 100644 --- a/src/google/adk/tools/environment/_read_file_tool.py +++ b/src/google/adk/tools/environment/_read_file_tool.py @@ -116,6 +116,8 @@ async def run_async( try: # TODO: Avoid loading the entire file into memory to prevent OOM on large files. data_bytes = await self._environment.read_file(path) + if not data_bytes and start_line is None and end_line is None: + return {'status': 'ok', 'content': ''} # Slice data_bytes by line boundaries before decoding. lines_bytes = data_bytes.splitlines(keepends=True) total = len(lines_bytes) diff --git a/tests/unittests/tools/environment/test_read_file_tool.py b/tests/unittests/tools/environment/test_read_file_tool.py index 8cc66d52785..158d5cb3952 100644 --- a/tests/unittests/tools/environment/test_read_file_tool.py +++ b/tests/unittests/tools/environment/test_read_file_tool.py @@ -112,6 +112,39 @@ async def _env(tmp_path: Path): class TestReadFileTool: """Tests for ReadFileTool behavior.""" + async def test_read_empty_file_without_line_range( + self, env: LocalEnvironment + ): + """Reading an existing empty file succeeds with empty content.""" + await env.write_file('empty.txt', b'') + + result = await ReadFileTool(env).run_async( + args={'path': 'empty.txt'}, tool_context=None + ) + + assert result == {'status': 'ok', 'content': ''} + + @pytest.mark.parametrize( + 'line_range', + [{'start_line': 1}, {'end_line': 1}, {'start_line': 2}], + ) + async def test_read_empty_file_with_line_range_returns_error( + self, env: LocalEnvironment, line_range + ): + """An explicit line range still reports that an empty file has no lines.""" + await env.write_file('empty.txt', b'') + start = line_range.get('start_line', 1) + + result = await ReadFileTool(env).run_async( + args={'path': 'empty.txt', **line_range}, tool_context=None + ) + + assert result == { + 'status': 'error', + 'error': f'`start_line` {start} exceeds file length (0 lines).', + 'total_lines': 0, + } + @pytest.mark.asyncio async def test_read_file_with_line_range_returns_selected_lines( self, env: LocalEnvironment From 638d932a9e339d8d968d1f48456ce4afbc973698 Mon Sep 17 00:00:00 2001 From: Mu-ty831 <3860902720@qq.com> Date: Sat, 17 Oct 2026 20:24:20 +0800 Subject: [PATCH 2/2] fix(tools): keep equivalent empty-file read ranges consistent Evaluate empty-file reads after normalizing the requested range so explicit defaults behave like omitted arguments. Preserve errors for out-of-range starts and negative end lines, and cover empty and nonempty files. --- .../adk/tools/environment/_read_file_tool.py | 4 +- .../tools/environment/test_read_file_tool.py | 43 ++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/google/adk/tools/environment/_read_file_tool.py b/src/google/adk/tools/environment/_read_file_tool.py index a7c3e9e6a79..6ed8a2f02bb 100644 --- a/src/google/adk/tools/environment/_read_file_tool.py +++ b/src/google/adk/tools/environment/_read_file_tool.py @@ -116,13 +116,13 @@ async def run_async( try: # TODO: Avoid loading the entire file into memory to prevent OOM on large files. data_bytes = await self._environment.read_file(path) - if not data_bytes and start_line is None and end_line is None: - return {'status': 'ok', 'content': ''} # Slice data_bytes by line boundaries before decoding. lines_bytes = data_bytes.splitlines(keepends=True) total = len(lines_bytes) start = max(1, start_line or 1) end = min(total, end_line or total) + if total == 0 and start == 1 and end == 0: + return {'status': 'ok', 'content': ''} if start > total: return { 'status': 'error', diff --git a/tests/unittests/tools/environment/test_read_file_tool.py b/tests/unittests/tools/environment/test_read_file_tool.py index 158d5cb3952..9168db72846 100644 --- a/tests/unittests/tools/environment/test_read_file_tool.py +++ b/tests/unittests/tools/environment/test_read_file_tool.py @@ -112,26 +112,55 @@ async def _env(tmp_path: Path): class TestReadFileTool: """Tests for ReadFileTool behavior.""" - async def test_read_empty_file_without_line_range( - self, env: LocalEnvironment + @pytest.mark.parametrize('content', [b'', b'a\nb\n']) + @pytest.mark.parametrize( + 'line_range', + [ + {}, + {'start_line': 1}, + {'start_line': 0}, + {'start_line': -1}, + {'end_line': 0}, + {'end_line': 5}, + {'start_line': 1, 'end_line': 100}, + ], + ) + async def test_read_whole_file_with_equivalent_line_ranges( + self, env: LocalEnvironment, content: bytes, line_range: dict[str, int] ): - """Reading an existing empty file succeeds with empty content.""" + """Equivalent whole-file ranges succeed for empty and nonempty files.""" + await env.write_file('sample.txt', content) + + result = await ReadFileTool(env).run_async( + args={'path': 'sample.txt', **line_range}, tool_context=None + ) + + expected = '' if not content else ' 1\ta\n 2\tb\n' + assert result == {'status': 'ok', 'content': expected} + + @pytest.mark.parametrize( + 'line_range', [{'end_line': 1}, {'start_line': 1, 'end_line': 1}] + ) + async def test_read_empty_file_through_first_line( + self, env: LocalEnvironment, line_range: dict[str, int] + ): + """An explicit first-line boundary also succeeds for an empty file.""" await env.write_file('empty.txt', b'') result = await ReadFileTool(env).run_async( - args={'path': 'empty.txt'}, tool_context=None + args={'path': 'empty.txt', **line_range}, tool_context=None ) assert result == {'status': 'ok', 'content': ''} @pytest.mark.parametrize( 'line_range', - [{'start_line': 1}, {'end_line': 1}, {'start_line': 2}], + [{'start_line': 2}, {'start_line': 2, 'end_line': 100}, {'end_line': -1}], ) async def test_read_empty_file_with_line_range_returns_error( - self, env: LocalEnvironment, line_range + self, env: LocalEnvironment, line_range: dict[str, int] ): - """An explicit line range still reports that an empty file has no lines.""" + """Invalid ranges still report that an empty file has no lines.""" await env.write_file('empty.txt', b'') start = line_range.get('start_line', 1)