mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-15 07:29:52 +00:00
Enhance file tools with read tracking, PDF support, safer path handling, smarter edit matching/diagnostics, and introduce notebook_edit with tests.
153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
"""Tests for EditFileTool enhancements: read-before-edit tracking, path suggestions,
|
|
.ipynb detection, and create-file semantics."""
|
|
|
|
import pytest
|
|
|
|
from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool, WriteFileTool
|
|
from nanobot.agent.tools import file_state
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_file_state():
|
|
"""Reset global read-state between tests."""
|
|
file_state.clear()
|
|
yield
|
|
file_state.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Read-before-edit tracking
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEditReadTracking:
|
|
"""edit_file should warn when file hasn't been read first."""
|
|
|
|
@pytest.fixture()
|
|
def read_tool(self, tmp_path):
|
|
return ReadFileTool(workspace=tmp_path)
|
|
|
|
@pytest.fixture()
|
|
def edit_tool(self, tmp_path):
|
|
return EditFileTool(workspace=tmp_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_warns_if_file_not_read_first(self, edit_tool, tmp_path):
|
|
f = tmp_path / "a.py"
|
|
f.write_text("hello world", encoding="utf-8")
|
|
result = await edit_tool.execute(path=str(f), old_text="world", new_text="earth")
|
|
# Should still succeed but include a warning
|
|
assert "Successfully" in result
|
|
assert "not been read" in result.lower() or "warning" in result.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_succeeds_cleanly_after_read(self, read_tool, edit_tool, tmp_path):
|
|
f = tmp_path / "a.py"
|
|
f.write_text("hello world", encoding="utf-8")
|
|
await read_tool.execute(path=str(f))
|
|
result = await edit_tool.execute(path=str(f), old_text="world", new_text="earth")
|
|
assert "Successfully" in result
|
|
# No warning when file was read first
|
|
assert "not been read" not in result.lower()
|
|
assert f.read_text() == "hello earth"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_warns_if_file_modified_since_read(self, read_tool, edit_tool, tmp_path):
|
|
f = tmp_path / "a.py"
|
|
f.write_text("hello world", encoding="utf-8")
|
|
await read_tool.execute(path=str(f))
|
|
# External modification
|
|
f.write_text("hello universe", encoding="utf-8")
|
|
result = await edit_tool.execute(path=str(f), old_text="universe", new_text="earth")
|
|
assert "Successfully" in result
|
|
assert "modified" in result.lower() or "warning" in result.lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create-file semantics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEditCreateFile:
|
|
"""edit_file with old_text='' creates new file if not exists."""
|
|
|
|
@pytest.fixture()
|
|
def tool(self, tmp_path):
|
|
return EditFileTool(workspace=tmp_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_new_file_with_empty_old_text(self, tool, tmp_path):
|
|
f = tmp_path / "subdir" / "new.py"
|
|
result = await tool.execute(path=str(f), old_text="", new_text="print('hi')")
|
|
assert "created" in result.lower() or "Successfully" in result
|
|
assert f.exists()
|
|
assert f.read_text() == "print('hi')"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_fails_if_file_already_exists_and_not_empty(self, tool, tmp_path):
|
|
f = tmp_path / "existing.py"
|
|
f.write_text("existing content", encoding="utf-8")
|
|
result = await tool.execute(path=str(f), old_text="", new_text="new content")
|
|
assert "Error" in result or "already exists" in result.lower()
|
|
# File should be unchanged
|
|
assert f.read_text() == "existing content"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_succeeds_if_file_exists_but_empty(self, tool, tmp_path):
|
|
f = tmp_path / "empty.py"
|
|
f.write_text("", encoding="utf-8")
|
|
result = await tool.execute(path=str(f), old_text="", new_text="print('hi')")
|
|
assert "Successfully" in result
|
|
assert f.read_text() == "print('hi')"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# .ipynb detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEditIpynbDetection:
|
|
"""edit_file should refuse .ipynb and suggest notebook_edit."""
|
|
|
|
@pytest.fixture()
|
|
def tool(self, tmp_path):
|
|
return EditFileTool(workspace=tmp_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ipynb_rejected_with_suggestion(self, tool, tmp_path):
|
|
f = tmp_path / "analysis.ipynb"
|
|
f.write_text('{"cells": []}', encoding="utf-8")
|
|
result = await tool.execute(path=str(f), old_text="x", new_text="y")
|
|
assert "notebook" in result.lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path suggestion on not-found
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEditPathSuggestion:
|
|
"""edit_file should suggest similar paths on not-found."""
|
|
|
|
@pytest.fixture()
|
|
def tool(self, tmp_path):
|
|
return EditFileTool(workspace=tmp_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_suggests_similar_filename(self, tool, tmp_path):
|
|
f = tmp_path / "config.py"
|
|
f.write_text("x = 1", encoding="utf-8")
|
|
# Typo: conifg.py
|
|
result = await tool.execute(
|
|
path=str(tmp_path / "conifg.py"), old_text="x = 1", new_text="x = 2",
|
|
)
|
|
assert "Error" in result
|
|
assert "config.py" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shows_cwd_in_error(self, tool, tmp_path):
|
|
result = await tool.execute(
|
|
path=str(tmp_path / "nonexistent.py"), old_text="a", new_text="b",
|
|
)
|
|
assert "Error" in result
|