from unittest.mock import mock_open, patch from crewai_tools import FileReadTool def test_file_read_tool_constructor(): """Test FileReadTool initialization with file_path.""" test_file = "test_file.txt" tool = FileReadTool(file_path=test_file) assert tool.file_path == test_file assert "test_file.txt" in tool.description def test_file_read_tool_run(): """Test FileReadTool _run method with file_path at runtime.""" test_file = "test_file.txt" test_content = "Hello, World!" # Use mock_open to mock file operations with patch("builtins.open", mock_open(read_data=test_content)): tool = FileReadTool() result = tool._run(file_path=test_file) assert result == test_content def test_file_read_tool_error_handling(): """Test FileReadTool error handling.""" tool = FileReadTool() result = tool._run() assert "Error: No file path provided" in result result = tool._run(file_path="nonexistent/file.txt") assert "Error: File not found at path:" in result with patch("builtins.open", side_effect=PermissionError()): result = tool._run(file_path="no_permission.txt") assert "Error: Permission denied" in result def test_file_read_tool_constructor_and_run(): """Test FileReadTool using both constructor and runtime file paths.""" test_file1 = "test1.txt" test_file2 = "test2.txt" content1 = "File 1 content" content2 = "File 2 content" with patch("builtins.open", mock_open(read_data=content1)): tool = FileReadTool(file_path=test_file1) result = tool._run() assert result == content1 # Then test with content2 (should override constructor file_path) with patch("builtins.open", mock_open(read_data=content2)): result = tool._run(file_path=test_file2) assert result == content2 def test_file_read_tool_chunk_reading(): """Test FileReadTool reading specific chunks of a file.""" test_file = "multiline_test.txt" lines = [ "Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n", "Line 6\n", "Line 7\n", "Line 8\n", "Line 9\n", "Line 10\n", ] file_content = "".join(lines) with patch("builtins.open", mock_open(read_data=file_content)): tool = FileReadTool() result = tool._run(file_path=test_file, start_line=3, line_count=3) expected = "".join(lines[2:5]) # Lines are 0-indexed in the array assert result == expected # Test reading from a specific line to the end result = tool._run(file_path=test_file, start_line=8) expected = "".join(lines[7:]) assert result == expected # Test with default values (should read entire file) result = tool._run(file_path=test_file) expected = "".join(lines) assert result == expected # Test when start_line is 1 but line_count is specified result = tool._run(file_path=test_file, start_line=1, line_count=5) expected = "".join(lines[0:5]) assert result == expected def test_file_read_tool_chunk_error_handling(): """Test error handling for chunk reading.""" test_file = "short_test.txt" lines = ["Line 1\n", "Line 2\n", "Line 3\n"] file_content = "".join(lines) with patch("builtins.open", mock_open(read_data=file_content)): tool = FileReadTool() result = tool._run(file_path=test_file, start_line=10) assert "Error: Start line 10 exceeds the number of lines in the file" in result # Test reading partial chunk when line_count exceeds available lines result = tool._run(file_path=test_file, start_line=2, line_count=10) expected = "".join(lines[1:]) # Should return from line 2 to end assert result == expected def test_file_read_tool_zero_or_negative_start_line(): """Test that start_line values of 0 or negative read from the start of the file.""" test_file = "negative_test.txt" lines = ["Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n"] file_content = "".join(lines) with patch("builtins.open", mock_open(read_data=file_content)): tool = FileReadTool() result = tool._run(file_path=test_file, start_line=None) expected = "".join(lines) # Should read the entire file assert result == expected result = tool._run(file_path=test_file, start_line=0) expected = "".join(lines) # Should read the entire file assert result == expected # Test with start_line = 0 and limited line count result = tool._run(file_path=test_file, start_line=0, line_count=3) expected = "".join(lines[0:3]) # Should read first 3 lines assert result == expected result = tool._run(file_path=test_file, start_line=-5) expected = "".join(lines) # Should read the entire file assert result == expected # Test with negative start_line and limited line count result = tool._run(file_path=test_file, start_line=-10, line_count=2) expected = "".join(lines[0:2]) # Should read first 2 lines assert result == expected def test_file_read_tool_error_messages_do_not_disclose_absolute_paths( tmp_path, monkeypatch ): """FileReadTool should redact absolute prefixes from user-visible errors.""" monkeypatch.chdir(tmp_path) tool = FileReadTool() target = tmp_path / "secret.txt" result = tool._run(file_path=str(target)) assert "secret.txt" in result assert str(tmp_path) not in result target.touch() with patch("builtins.open", side_effect=PermissionError()): result = tool._run(file_path=str(target)) assert "secret.txt" in result assert str(tmp_path) not in result with patch( "builtins.open", side_effect=OSError(5, "Input/output error", str(target)), ): result = tool._run(file_path=str(target)) assert "secret.txt" in result assert str(tmp_path) not in result def test_file_read_tool_invalid_path_error_does_not_disclose_workspace( tmp_path, monkeypatch ): """Validation errors should not echo the resolved workspace path.""" monkeypatch.chdir(tmp_path) outside = tmp_path.parent / "outside.txt" result = FileReadTool()._run(file_path=str(outside)) assert "Invalid file path" in result assert "outside.txt" in result assert str(tmp_path) not in result assert str(tmp_path.parent) not in result