"""Tests for EditFileTool enhancements: read-before-edit tracking, path suggestions, notebook JSON editing, 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 file_states(self): return file_state.FileStates() @pytest.fixture() def read_tool(self, tmp_path, file_states): return ReadFileTool(workspace=tmp_path, file_states=file_states) @pytest.fixture() def edit_tool(self, tmp_path, file_states): return EditFileTool(workspace=tmp_path, file_states=file_states) @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 editing # --------------------------------------------------------------------------- class TestEditIpynbFiles: """edit_file edits notebooks as normal JSON files.""" @pytest.fixture() def tool(self, tmp_path): return EditFileTool(workspace=tmp_path) @pytest.mark.asyncio async def test_ipynb_can_be_edited_as_json(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='"cells": []', new_text='"cells": [{"cell_type": "markdown", "source": "hi"}]', ) assert "Successfully edited" in result assert '"source": "hi"' in f.read_text(encoding="utf-8") # --------------------------------------------------------------------------- # 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