diff --git a/src/google/adk/tools/environment/_read_file_tool.py b/src/google/adk/tools/environment/_read_file_tool.py index d49dd0d054a..6ed8a2f02bb 100644 --- a/src/google/adk/tools/environment/_read_file_tool.py +++ b/src/google/adk/tools/environment/_read_file_tool.py @@ -121,6 +121,8 @@ async def run_async( 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 8cc66d52785..9168db72846 100644 --- a/tests/unittests/tools/environment/test_read_file_tool.py +++ b/tests/unittests/tools/environment/test_read_file_tool.py @@ -112,6 +112,68 @@ async def _env(tmp_path: Path): class TestReadFileTool: """Tests for ReadFileTool behavior.""" + @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] + ): + """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', **line_range}, tool_context=None + ) + + assert result == {'status': 'ok', 'content': ''} + + @pytest.mark.parametrize( + 'line_range', + [{'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: dict[str, int] + ): + """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) + + 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