ReMe/tests/light/test_tools.py

321 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name
"""Unit tests for Shell and FileIO tools."""
import asyncio
import os
import shutil
import tempfile
import pytest
from reme.memory.file_based.tools.file_io import FileIO
from reme.memory.file_based.tools.shell import Shell
from reme.memory.file_based.tools.utils import DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES
# ============ Shell Tests ============
@pytest.fixture(scope="module")
def shell_env():
"""Create temporary directory and Shell instance."""
test_dir = tempfile.mkdtemp(prefix="test_shell_")
shell = Shell(working_dir=test_dir)
yield {"dir": test_dir, "shell": shell}
shutil.rmtree(test_dir, ignore_errors=True)
def test_shell_echo_success(shell_env):
"""Test successful echo command execution."""
result = asyncio.run(shell_env["shell"].execute_shell_command("echo hello"))
assert result.content
text = result.content[0].get("text", "")
assert "hello" in text
def test_shell_pwd_in_working_dir(shell_env):
"""Test command executes in correct working directory."""
result = asyncio.run(shell_env["shell"].execute_shell_command("pwd"))
text = result.content[0].get("text", "")
assert shell_env["dir"] in text
def test_shell_command_failure(shell_env):
"""Test failed command returns error information."""
result = asyncio.run(shell_env["shell"].execute_shell_command("exit 1"))
text = result.content[0].get("text", "")
assert "failed" in text.lower()
assert "exit code" in text.lower()
def test_shell_command_with_stderr(shell_env):
"""Test command with stderr output."""
result = asyncio.run(
shell_env["shell"].execute_shell_command("echo error >&2 && exit 1"),
)
text = result.content[0].get("text", "")
assert "error" in text
def test_shell_no_output(shell_env):
"""Test successful command with no output."""
result = asyncio.run(shell_env["shell"].execute_shell_command("true"))
text = result.content[0].get("text", "")
assert "successfully" in text.lower()
def test_shell_multiline_output(shell_env):
"""Test command with multiline output."""
result = asyncio.run(
shell_env["shell"].execute_shell_command("echo -e 'line1\nline2\nline3'"),
)
text = result.content[0].get("text", "")
assert "line1" in text
assert "line2" in text
assert "line3" in text
def test_shell_truncated_output(shell_env):
"""Test output truncation for large output."""
lines_to_generate = DEFAULT_MAX_LINES + 500
cmd = f"seq 1 {lines_to_generate}"
result = asyncio.run(shell_env["shell"].execute_shell_command(cmd))
text = result.content[0].get("text", "")
# Should contain truncation notice
assert "truncated" in text.lower()
# Should contain the last line (tail is kept)
assert str(lines_to_generate) in text
# Verify first numeric line is > 1 (truncated from head)
numeric_lines = [tl for tl in text.strip().split("\n") if tl.isdigit()]
if numeric_lines:
assert int(numeric_lines[0]) > 1
def test_shell_timeout(shell_env):
"""Test command timeout handling."""
result = asyncio.run(
shell_env["shell"].execute_shell_command("sleep 10", timeout=1),
)
text = result.content[0].get("text", "")
assert "timeout" in text.lower()
# ============ FileIO Read Tests ============
@pytest.fixture(scope="module")
def fileio_env():
"""Create temporary directory with test files."""
test_dir = tempfile.mkdtemp(prefix="test_fileio_")
file_io = FileIO(working_dir=test_dir)
# Create simple test file
simple_file = os.path.join(test_dir, "simple.txt")
with open(simple_file, "w", encoding="utf-8") as f:
f.write("line1\nline2\nline3\nline4\nline5")
# Create large file (exceeds DEFAULT_MAX_LINES)
large_file = os.path.join(test_dir, "large.txt")
with open(large_file, "w", encoding="utf-8") as f:
for i in range(1, DEFAULT_MAX_LINES + 500):
f.write(f"line {i}\n")
# Create large bytes file (exceeds DEFAULT_MAX_BYTES)
large_bytes_file = os.path.join(test_dir, "large_bytes.txt")
with open(large_bytes_file, "w", encoding="utf-8") as f:
content = "x" * 100 + "\n"
lines_needed = (DEFAULT_MAX_BYTES // 101) + 100
for _ in range(lines_needed):
f.write(content)
yield {
"dir": test_dir,
"file_io": file_io,
"simple_file": simple_file,
"large_file": large_file,
"large_bytes_file": large_bytes_file,
}
shutil.rmtree(test_dir, ignore_errors=True)
def test_read_file_success(fileio_env):
"""Test successful file reading."""
result = asyncio.run(fileio_env["file_io"].read(fileio_env["simple_file"]))
text = result.content[0].get("text", "")
assert "line1" in text
assert "line5" in text
def test_read_file_relative_path(fileio_env):
"""Test reading file with relative path."""
result = asyncio.run(fileio_env["file_io"].read("simple.txt"))
text = result.content[0].get("text", "")
assert "line1" in text
def test_read_file_not_exists(fileio_env):
"""Test reading non-existent file."""
result = asyncio.run(fileio_env["file_io"].read("nonexistent.txt"))
text = result.content[0].get("text", "")
assert "Error" in text
assert "does not exist" in text
def test_read_file_with_line_range(fileio_env):
"""Test reading specific line range."""
result = asyncio.run(
fileio_env["file_io"].read(fileio_env["simple_file"], start_line=2, end_line=4),
)
text = result.content[0].get("text", "")
assert "line2" in text
assert "line4" in text
assert "lines 2-4" in text.lower()
def test_read_file_start_line_exceeds(fileio_env):
"""Test start_line exceeding file length."""
result = asyncio.run(
fileio_env["file_io"].read(fileio_env["simple_file"], start_line=100),
)
text = result.content[0].get("text", "")
assert "Error" in text
assert "exceeds" in text
def test_read_file_invalid_range(fileio_env):
"""Test invalid line range (start > end)."""
result = asyncio.run(
fileio_env["file_io"].read(fileio_env["simple_file"], start_line=4, end_line=2),
)
text = result.content[0].get("text", "")
assert "Error" in text
def test_read_file_truncated_by_lines(fileio_env):
"""Test file truncation by line limit."""
result = asyncio.run(fileio_env["file_io"].read(fileio_env["large_file"]))
text = result.content[0].get("text", "")
assert "line 1" in text # Head is kept
assert "continue" in text.lower()
def test_read_file_truncated_by_bytes(fileio_env):
"""Test file truncation by byte limit."""
result = asyncio.run(fileio_env["file_io"].read(fileio_env["large_bytes_file"]))
text = result.content[0].get("text", "")
assert "continue" in text.lower() or "KB" in text
def test_read_directory_error(fileio_env):
"""Test reading a directory returns error."""
result = asyncio.run(fileio_env["file_io"].read(fileio_env["dir"]))
text = result.content[0].get("text", "")
assert "Error" in text
assert "not a file" in text
# ============ FileIO Write Tests ============
@pytest.fixture
def write_env():
"""Create temporary directory for write tests."""
test_dir = tempfile.mkdtemp(prefix="test_fileio_write_")
file_io = FileIO(working_dir=test_dir)
yield {"dir": test_dir, "file_io": file_io}
shutil.rmtree(test_dir, ignore_errors=True)
def test_write_new_file(write_env):
"""Test writing a new file."""
file_path = os.path.join(write_env["dir"], "new_file.txt")
result = asyncio.run(write_env["file_io"].write(file_path, "test content"))
text = result.content[0].get("text", "")
assert "Wrote" in text
with open(file_path, "r", encoding="utf-8") as f:
assert f.read() == "test content"
def test_write_overwrite_file(write_env):
"""Test overwriting existing file."""
file_path = os.path.join(write_env["dir"], "overwrite.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write("old content")
result = asyncio.run(write_env["file_io"].write(file_path, "new content"))
text = result.content[0].get("text", "")
assert "Wrote" in text
with open(file_path, "r", encoding="utf-8") as f:
assert f.read() == "new content"
def test_write_empty_path(write_env):
"""Test writing with empty path."""
result = asyncio.run(write_env["file_io"].write("", "content"))
text = result.content[0].get("text", "")
assert "Error" in text
def test_write_relative_path(write_env):
"""Test writing file with relative path."""
result = asyncio.run(write_env["file_io"].write("relative.txt", "relative content"))
text = result.content[0].get("text", "")
assert "Wrote" in text
file_path = os.path.join(write_env["dir"], "relative.txt")
assert os.path.exists(file_path)
# ============ FileIO Edit Tests ============
@pytest.fixture
def edit_env():
"""Create temporary directory with edit test file."""
test_dir = tempfile.mkdtemp(prefix="test_fileio_edit_")
file_io = FileIO(working_dir=test_dir)
edit_file = os.path.join(test_dir, "edit_test.txt")
with open(edit_file, "w", encoding="utf-8") as f:
f.write("Hello World\nThis is a test\nHello Again")
yield {"dir": test_dir, "file_io": file_io, "edit_file": edit_file}
shutil.rmtree(test_dir, ignore_errors=True)
def test_edit_replace_text(edit_env):
"""Test replacing text in file."""
result = asyncio.run(
edit_env["file_io"].edit(edit_env["edit_file"], "Hello", "Hi"),
)
text = result.content[0].get("text", "")
assert "Successfully" in text
with open(edit_env["edit_file"], "r", encoding="utf-8") as f:
content = f.read()
assert "Hello" not in content
assert "Hi World" in content
assert "Hi Again" in content
def test_edit_text_not_found(edit_env):
"""Test editing when text not found."""
result = asyncio.run(
edit_env["file_io"].edit(edit_env["edit_file"], "NotExists", "Replacement"),
)
text = result.content[0].get("text", "")
assert "Error" in text
assert "not found" in text
def test_edit_nonexistent_file(edit_env):
"""Test editing non-existent file."""
result = asyncio.run(
edit_env["file_io"].edit("nonexistent.txt", "old", "new"),
)
text = result.content[0].get("text", "")
assert "Error" in text