mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-05 01:02:37 +00:00
When a developer sets base_dir, they control where files are written. The LLM should only supply filename and content — not a directory path. Adds ScopedFileWriterToolInput (no directory field) which is used when base_dir is provided at construction, following the same pattern as FileReadTool/ScrapeWebsiteTool. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
5.4 KiB
Python
198 lines
5.4 KiB
Python
import os
|
|
import shutil
|
|
import tempfile
|
|
|
|
from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def tool():
|
|
return FileWriterTool()
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_env():
|
|
temp_dir = tempfile.mkdtemp()
|
|
test_file = "test.txt"
|
|
test_content = "Hello, World!"
|
|
|
|
yield {
|
|
"temp_dir": temp_dir,
|
|
"test_file": test_file,
|
|
"test_content": test_content,
|
|
}
|
|
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
|
|
def get_test_path(filename, directory):
|
|
return os.path.join(directory, filename)
|
|
|
|
|
|
def read_file(path):
|
|
with open(path, "r") as f:
|
|
return f.read()
|
|
|
|
|
|
def test_basic_file_write(tool, temp_env):
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=temp_env["temp_dir"],
|
|
content=temp_env["test_content"],
|
|
overwrite=True,
|
|
)
|
|
|
|
path = get_test_path(temp_env["test_file"], temp_env["temp_dir"])
|
|
assert os.path.exists(path)
|
|
assert read_file(path) == temp_env["test_content"]
|
|
assert "successfully written" in result
|
|
|
|
|
|
def test_directory_creation(tool, temp_env):
|
|
new_dir = os.path.join(temp_env["temp_dir"], "nested_dir")
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=new_dir,
|
|
content=temp_env["test_content"],
|
|
overwrite=True,
|
|
)
|
|
|
|
path = get_test_path(temp_env["test_file"], new_dir)
|
|
assert os.path.exists(new_dir)
|
|
assert os.path.exists(path)
|
|
assert "successfully written" in result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"overwrite",
|
|
["y", "yes", "t", "true", "on", "1", True],
|
|
)
|
|
def test_overwrite_true(tool, temp_env, overwrite):
|
|
path = get_test_path(temp_env["test_file"], temp_env["temp_dir"])
|
|
with open(path, "w") as f:
|
|
f.write("Original content")
|
|
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=temp_env["temp_dir"],
|
|
content="New content",
|
|
overwrite=overwrite,
|
|
)
|
|
|
|
assert read_file(path) == "New content"
|
|
assert "successfully written" in result
|
|
|
|
|
|
def test_invalid_overwrite_value(tool, temp_env):
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=temp_env["temp_dir"],
|
|
content=temp_env["test_content"],
|
|
overwrite="invalid",
|
|
)
|
|
assert "invalid value" in result
|
|
|
|
|
|
def test_missing_required_fields(tool, temp_env):
|
|
result = tool._run(
|
|
directory=temp_env["temp_dir"],
|
|
content=temp_env["test_content"],
|
|
overwrite=True,
|
|
)
|
|
assert "An error occurred while accessing key: 'filename'" in result
|
|
|
|
|
|
def test_empty_content(tool, temp_env):
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=temp_env["temp_dir"],
|
|
content="",
|
|
overwrite=True,
|
|
)
|
|
|
|
path = get_test_path(temp_env["test_file"], temp_env["temp_dir"])
|
|
assert os.path.exists(path)
|
|
assert read_file(path) == ""
|
|
assert "successfully written" in result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"overwrite",
|
|
["n", "no", "f", "false", "off", "0", False],
|
|
)
|
|
def test_file_exists_error_handling(tool, temp_env, overwrite):
|
|
path = get_test_path(temp_env["test_file"], temp_env["temp_dir"])
|
|
with open(path, "w") as f:
|
|
f.write("Pre-existing content")
|
|
|
|
result = tool._run(
|
|
filename=temp_env["test_file"],
|
|
directory=temp_env["temp_dir"],
|
|
content="Should not be written",
|
|
overwrite=overwrite,
|
|
)
|
|
|
|
assert "already exists and overwrite option was not passed" in result
|
|
assert read_file(path) == "Pre-existing content"
|
|
|
|
|
|
# --- base_dir containment ---
|
|
|
|
@pytest.fixture
|
|
def scoped_tool(temp_env):
|
|
return FileWriterTool(base_dir=temp_env["temp_dir"])
|
|
|
|
|
|
def test_base_dir_schema_has_no_directory_field(temp_env):
|
|
"""When base_dir is set, the LLM schema has no directory field."""
|
|
from crewai_tools.tools.file_writer_tool.file_writer_tool import ScopedFileWriterToolInput
|
|
tool = FileWriterTool(base_dir=temp_env["temp_dir"])
|
|
assert tool.args_schema is ScopedFileWriterToolInput
|
|
assert "directory" not in tool.args_schema.model_fields
|
|
|
|
|
|
def test_base_dir_allows_write_inside(scoped_tool, temp_env):
|
|
"""LLM supplies only filename — file lands in base_dir."""
|
|
result = scoped_tool._run(
|
|
filename=temp_env["test_file"],
|
|
content=temp_env["test_content"],
|
|
overwrite=True,
|
|
)
|
|
assert "successfully written" in result
|
|
assert read_file(get_test_path(temp_env["test_file"], temp_env["temp_dir"])) == temp_env["test_content"]
|
|
|
|
|
|
def test_base_dir_blocks_traversal_in_filename(scoped_tool, temp_env):
|
|
result = scoped_tool._run(
|
|
filename="../outside.txt",
|
|
content="should not be written",
|
|
overwrite=True,
|
|
)
|
|
assert "Access denied" in result
|
|
|
|
|
|
def test_base_dir_blocks_absolute_filename(scoped_tool, temp_env):
|
|
result = scoped_tool._run(
|
|
filename="/etc/passwd",
|
|
content="should not be written",
|
|
overwrite=True,
|
|
)
|
|
assert "Access denied" in result
|
|
|
|
|
|
def test_base_dir_blocks_symlink_escape(scoped_tool, temp_env):
|
|
link = os.path.join(temp_env["temp_dir"], "escape")
|
|
os.symlink("/etc", link)
|
|
result = scoped_tool._run(
|
|
filename="escape/crontab",
|
|
content="should not be written",
|
|
overwrite=True,
|
|
)
|
|
assert "Access denied" in result
|
|
|
|
|
|
def test_base_dir_description_mentions_directory(temp_env):
|
|
tool = FileWriterTool(base_dir=temp_env["temp_dir"])
|
|
assert temp_env["temp_dir"] in tool.description
|