multi-agent-sdlc / tests /tools /coder /test_filesystem.py
Pedro
reorganize workflow nodes and tests
5d06dbc
Raw History Blame Contribute Delete
22 kB
from pathlib import Path
from typing import Any, Callable
import pytest
from langchain.tools import ToolRuntime
from langchain_core.tools import BaseTool, StructuredTool
from multi_agent_sdlc.tools.coder.filesystem import (
coder_create_directory,
coder_delete_directory,
coder_delete_file,
coder_move_path,
coder_write_file,
)
from multi_agent_sdlc.workflow.state import DevState
def get_tool_function(
tool: BaseTool,
) -> Callable[..., Any]:
if not isinstance(tool, StructuredTool):
raise TypeError(f"Expected StructuredTool, got {type(tool).__name__}")
if tool.func is None:
raise TypeError(f"Tool {tool.name} does not have a synchronous function")
return tool.func
def test_write_file_creates_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_write_file)
result = function(
path="main.py",
content="print('hello')\n",
runtime=tool_runtime,
)
file_path = project_directory / "main.py"
assert file_path.is_file()
assert file_path.read_text(encoding="utf-8") == "print('hello')\n"
assert result == "Written production file: main.py"
def test_write_file_creates_parent_directories(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_write_file)
function(
path="src/services/main.py",
content="value = 1\n",
runtime=tool_runtime,
)
file_path = project_directory / "src/services/main.py"
assert file_path.is_file()
assert file_path.read_text(encoding="utf-8") == "value = 1\n"
def test_write_file_overwrites_existing_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
file_path = project_directory / "main.py"
file_path.write_text("old", encoding="utf-8")
function = get_tool_function(coder_write_file)
function(
path="main.py",
content="new",
runtime=tool_runtime,
)
assert file_path.read_text(encoding="utf-8") == "new"
def test_write_file_rejects_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "main.py"
directory.mkdir()
function = get_tool_function(coder_write_file)
with pytest.raises(IsADirectoryError):
function(
path="main.py",
content="new",
runtime=tool_runtime,
)
assert directory.is_dir()
def test_write_file_rejects_test_path(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_write_file)
with pytest.raises(PermissionError):
function(
path="tests/test_main.py",
content="",
runtime=tool_runtime,
)
def test_write_file_rejects_path_escape(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_write_file)
with pytest.raises(PermissionError):
function(
path="../outside.py",
content="",
runtime=tool_runtime,
)
def test_create_directory_creates_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_create_directory)
result = function(
path="src/services",
runtime=tool_runtime,
)
assert (project_directory / "src/services").is_dir()
assert result == "Created production directory: src/services"
def test_create_directory_creates_parent_directories(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_create_directory)
function(
path="src/services/internal",
runtime=tool_runtime,
)
assert (project_directory / "src/services/internal").is_dir()
def test_create_directory_allows_existing_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "src"
directory.mkdir()
function = get_tool_function(coder_create_directory)
result = function(
path="src",
runtime=tool_runtime,
)
assert directory.is_dir()
assert result == "Created production directory: src"
def test_create_directory_rejects_existing_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
file_path = project_directory / "src"
file_path.write_text("content", encoding="utf-8")
function = get_tool_function(coder_create_directory)
with pytest.raises(FileExistsError):
function(
path="src",
runtime=tool_runtime,
)
assert file_path.is_file()
def test_create_directory_rejects_test_path(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_create_directory)
with pytest.raises(PermissionError):
function(
path="tests/unit",
runtime=tool_runtime,
)
def test_move_path_renames_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source = project_directory / "old.py"
source.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_move_path)
result = function(
source_path="old.py",
destination_path="new.py",
runtime=tool_runtime,
)
destination = project_directory / "new.py"
assert not source.exists()
assert destination.is_file()
assert destination.read_text(encoding="utf-8") == "value = 1"
assert result == "Moved production path: old.py -> new.py"
def test_move_path_moves_file_between_directories(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source_directory = project_directory / "src"
destination_directory = project_directory / "app"
source_directory.mkdir()
destination_directory.mkdir()
source = source_directory / "main.py"
source.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_move_path)
function(
source_path="src/main.py",
destination_path="app/main.py",
runtime=tool_runtime,
)
destination = destination_directory / "main.py"
assert not source.exists()
assert destination.is_file()
assert destination.read_text(encoding="utf-8") == "value = 1"
def test_move_path_renames_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source = project_directory / "old_package"
source.mkdir()
(source / "module.py").write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_move_path)
function(
source_path="old_package",
destination_path="new_package",
runtime=tool_runtime,
)
destination = project_directory / "new_package"
assert not source.exists()
assert destination.is_dir()
assert (destination / "module.py").is_file()
def test_move_path_rejects_missing_source(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_move_path)
with pytest.raises(FileNotFoundError):
function(
source_path="missing.py",
destination_path="new.py",
runtime=tool_runtime,
)
def test_move_path_rejects_existing_destination(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source = project_directory / "old.py"
destination = project_directory / "new.py"
source.write_text("old", encoding="utf-8")
destination.write_text("new", encoding="utf-8")
function = get_tool_function(coder_move_path)
with pytest.raises(FileExistsError):
function(
source_path="old.py",
destination_path="new.py",
runtime=tool_runtime,
)
assert source.exists()
assert destination.read_text(encoding="utf-8") == "new"
def test_move_path_rejects_missing_destination_parent(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source = project_directory / "main.py"
source.write_text("", encoding="utf-8")
function = get_tool_function(coder_move_path)
with pytest.raises(FileNotFoundError):
function(
source_path="main.py",
destination_path="app/main.py",
runtime=tool_runtime,
)
assert source.exists()
def test_move_path_rejects_directory_inside_itself(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "app"
directory.mkdir()
function = get_tool_function(coder_move_path)
with pytest.raises(ValueError):
function(
source_path="app",
destination_path="app/internal",
runtime=tool_runtime,
)
assert directory.is_dir()
def test_move_path_rejects_test_source(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
tests_directory = project_directory / "tests"
tests_directory.mkdir()
test_file = tests_directory / "test_app.py"
test_file.write_text("", encoding="utf-8")
function = get_tool_function(coder_move_path)
with pytest.raises(PermissionError):
function(
source_path="tests/test_app.py",
destination_path="test_app.py",
runtime=tool_runtime,
)
assert test_file.exists()
def test_move_path_rejects_test_destination(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
source = project_directory / "main.py"
source.write_text("", encoding="utf-8")
function = get_tool_function(coder_move_path)
with pytest.raises(PermissionError):
function(
source_path="main.py",
destination_path="tests/main.py",
runtime=tool_runtime,
)
assert source.exists()
def test_delete_file_deletes_existing_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
file_path = project_directory / "obsolete.py"
file_path.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_delete_file)
result = function(
path="obsolete.py",
runtime=tool_runtime,
)
assert not file_path.exists()
assert result == "Deleted production file: obsolete.py"
def test_delete_file_deletes_nested_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "src" / "legacy"
directory.mkdir(parents=True)
file_path = directory / "obsolete.py"
file_path.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_delete_file)
function(
path="src/legacy/obsolete.py",
runtime=tool_runtime,
)
assert not file_path.exists()
assert directory.is_dir()
def test_delete_file_does_not_delete_sibling(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "src"
directory.mkdir()
target = directory / "obsolete.py"
sibling = directory / "keep.py"
target.write_text("delete", encoding="utf-8")
sibling.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_file)
function(
path="src/obsolete.py",
runtime=tool_runtime,
)
assert not target.exists()
assert sibling.exists()
assert sibling.read_text(encoding="utf-8") == "keep"
assert directory.is_dir()
def test_delete_file_rejects_missing_file(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_delete_file)
with pytest.raises(FileNotFoundError):
function(
path="missing.py",
runtime=tool_runtime,
)
def test_delete_file_rejects_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "src"
directory.mkdir()
marker = directory / "marker.py"
marker.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_file)
with pytest.raises(IsADirectoryError):
function(
path="src",
runtime=tool_runtime,
)
assert directory.is_dir()
assert marker.exists()
def test_delete_file_rejects_test_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
tests_directory = project_directory / "tests"
tests_directory.mkdir()
test_file = tests_directory / "test_app.py"
test_file.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_file)
with pytest.raises(PermissionError):
function(
path="tests/test_app.py",
runtime=tool_runtime,
)
assert test_file.exists()
assert test_file.read_text(encoding="utf-8") == "keep"
@pytest.mark.parametrize(
"path",
[
"../outside.py",
"../../outside.py",
"/tmp/outside.py",
],
)
def test_delete_file_rejects_path_escape(
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
function = get_tool_function(coder_delete_file)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
@pytest.mark.parametrize(
"path",
[
"terminal-calculator/file.py",
"sandbox/terminal-calculator/file.py",
],
)
def test_delete_file_rejects_project_prefix(
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
function = get_tool_function(coder_delete_file)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
def test_delete_file_rejects_external_symlink(
tmp_path: Path,
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
outside_file = tmp_path / "outside.py"
outside_file.write_text("keep", encoding="utf-8")
alias = project_directory / "external.py"
alias.symlink_to(outside_file)
function = get_tool_function(coder_delete_file)
with pytest.raises(PermissionError):
function(
path="external.py",
runtime=tool_runtime,
)
assert outside_file.exists()
assert outside_file.read_text(encoding="utf-8") == "keep"
assert alias.is_symlink()
def test_delete_file_allows_internal_symlink_target(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
target = project_directory / "real.py"
target.write_text("value = 1", encoding="utf-8")
alias = project_directory / "alias.py"
alias.symlink_to(target)
function = get_tool_function(coder_delete_file)
function(
path="alias.py",
runtime=tool_runtime,
)
assert not target.exists()
assert alias.is_symlink()
def test_delete_directory_deletes_empty_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "obsolete"
directory.mkdir()
function = get_tool_function(coder_delete_directory)
result = function(
path="obsolete",
runtime=tool_runtime,
)
assert not directory.exists()
assert result == "Deleted production directory: obsolete"
def test_delete_directory_deletes_nonempty_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "obsolete"
directory.mkdir()
file_path = directory / "module.py"
file_path.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
function(
path="obsolete",
runtime=tool_runtime,
)
assert not directory.exists()
assert not file_path.exists()
def test_delete_directory_deletes_nested_tree(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
directory = project_directory / "obsolete"
nested_directory = directory / "package" / "internal"
nested_directory.mkdir(parents=True)
root_file = directory / "README.md"
nested_file = nested_directory / "module.py"
root_file.write_text("obsolete", encoding="utf-8")
nested_file.write_text("value = 1", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
function(
path="obsolete",
runtime=tool_runtime,
)
assert not directory.exists()
assert not root_file.exists()
assert not nested_directory.exists()
assert not nested_file.exists()
def test_delete_directory_does_not_delete_sibling(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
target = project_directory / "obsolete"
sibling = project_directory / "keep"
target.mkdir()
sibling.mkdir()
target_file = target / "delete.py"
sibling_file = sibling / "keep.py"
target_file.write_text("delete", encoding="utf-8")
sibling_file.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
function(
path="obsolete",
runtime=tool_runtime,
)
assert not target.exists()
assert sibling.is_dir()
assert sibling_file.exists()
assert sibling_file.read_text(encoding="utf-8") == "keep"
def test_delete_directory_rejects_missing_directory(
tool_runtime: ToolRuntime[DevState],
) -> None:
function = get_tool_function(coder_delete_directory)
with pytest.raises(FileNotFoundError):
function(
path="missing",
runtime=tool_runtime,
)
def test_delete_directory_rejects_file(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
file_path = project_directory / "main.py"
file_path.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
with pytest.raises(NotADirectoryError):
function(
path="main.py",
runtime=tool_runtime,
)
assert file_path.exists()
assert file_path.read_text(encoding="utf-8") == "keep"
@pytest.mark.parametrize(
"path",
[
".",
"./",
],
)
def test_delete_directory_rejects_project_root(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
marker = project_directory / "marker.txt"
marker.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
assert project_directory.is_dir()
assert marker.exists()
assert marker.read_text(encoding="utf-8") == "keep"
@pytest.mark.parametrize(
"path",
[
"tests",
"tests/unit",
"tests/integration",
],
)
def test_delete_directory_rejects_test_directory(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
target = project_directory / path
target.mkdir(parents=True)
marker = target / "test_marker.py"
marker.write_text("keep", encoding="utf-8")
function = get_tool_function(coder_delete_directory)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
assert target.is_dir()
assert marker.exists()
assert marker.read_text(encoding="utf-8") == "keep"
@pytest.mark.parametrize(
"path",
[
"../outside",
"../../outside",
"/tmp/outside",
],
)
def test_delete_directory_rejects_path_escape(
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
function = get_tool_function(coder_delete_directory)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
@pytest.mark.parametrize(
"path",
[
"terminal-calculator/obsolete",
"sandbox/terminal-calculator/obsolete",
],
)
def test_delete_directory_rejects_project_prefix(
tool_runtime: ToolRuntime[DevState],
path: str,
) -> None:
function = get_tool_function(coder_delete_directory)
with pytest.raises(PermissionError):
function(
path=path,
runtime=tool_runtime,
)
def test_delete_directory_rejects_external_symlink(
tmp_path: Path,
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
outside_directory = tmp_path / "outside"
outside_directory.mkdir()
marker = outside_directory / "marker.txt"
marker.write_text("keep", encoding="utf-8")
alias = project_directory / "external"
alias.symlink_to(
outside_directory,
target_is_directory=True,
)
function = get_tool_function(coder_delete_directory)
with pytest.raises(PermissionError):
function(
path="external",
runtime=tool_runtime,
)
assert outside_directory.is_dir()
assert marker.exists()
assert marker.read_text(encoding="utf-8") == "keep"
assert alias.is_symlink()
def test_delete_directory_allows_internal_symlink_target(
project_directory: Path,
tool_runtime: ToolRuntime[DevState],
) -> None:
target = project_directory / "real_directory"
target.mkdir()
target_file = target / "module.py"
target_file.write_text("value = 1", encoding="utf-8")
alias = project_directory / "alias"
alias.symlink_to(
target,
target_is_directory=True,
)
function = get_tool_function(coder_delete_directory)
function(
path="alias",
runtime=tool_runtime,
)
assert not target.exists()
assert not target_file.exists()
assert alias.is_symlink()