2026-02-26 00:53:57 -08:00
|
|
|
"""Tests for the file tools module (schema, handler wiring, error paths).
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
Tests verify tool schemas, handler dispatch, validation logic, and error
|
|
|
|
|
handling without requiring a running terminal environment.
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import (
|
|
|
|
|
FILE_TOOLS,
|
|
|
|
|
READ_FILE_SCHEMA,
|
|
|
|
|
WRITE_FILE_SCHEMA,
|
|
|
|
|
PATCH_SCHEMA,
|
|
|
|
|
SEARCH_FILES_SCHEMA,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFileToolsList:
|
2026-02-26 00:53:57 -08:00
|
|
|
def test_has_expected_entries(self):
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
names = {t["name"] for t in FILE_TOOLS}
|
|
|
|
|
assert names == {"read_file", "write_file", "patch", "search_files"}
|
|
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
def test_each_entry_has_callable_function(self):
|
|
|
|
|
for tool in FILE_TOOLS:
|
|
|
|
|
assert callable(tool["function"]), f"{tool['name']} missing callable"
|
|
|
|
|
|
|
|
|
|
def test_schemas_have_required_fields(self):
|
|
|
|
|
"""All schemas must have name, description, and parameters with properties."""
|
|
|
|
|
for schema in [READ_FILE_SCHEMA, WRITE_FILE_SCHEMA, PATCH_SCHEMA, SEARCH_FILES_SCHEMA]:
|
|
|
|
|
assert "name" in schema
|
|
|
|
|
assert "description" in schema
|
|
|
|
|
assert "properties" in schema["parameters"]
|
|
|
|
|
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
|
|
|
|
class TestReadFileHandler:
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
2026-02-26 00:53:57 -08:00
|
|
|
def test_returns_file_content(self, mock_get):
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
2026-02-26 00:53:57 -08:00
|
|
|
result_obj.to_dict.return_value = {"content": "line1\nline2", "total_lines": 2}
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
mock_ops.read_file.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import read_file_tool
|
|
|
|
|
result = json.loads(read_file_tool("/tmp/test.txt"))
|
2026-02-26 00:53:57 -08:00
|
|
|
assert result["content"] == "line1\nline2"
|
|
|
|
|
assert result["total_lines"] == 2
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
mock_ops.read_file.assert_called_once_with("/tmp/test.txt", 1, 500)
|
|
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_custom_offset_and_limit(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"content": "line10", "total_lines": 50}
|
|
|
|
|
mock_ops.read_file.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import read_file_tool
|
|
|
|
|
read_file_tool("/tmp/big.txt", offset=10, limit=20)
|
|
|
|
|
mock_ops.read_file.assert_called_once_with("/tmp/big.txt", 10, 20)
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_exception_returns_error_json(self, mock_get):
|
|
|
|
|
mock_get.side_effect = RuntimeError("terminal not available")
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import read_file_tool
|
|
|
|
|
result = json.loads(read_file_tool("/tmp/test.txt"))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
assert "terminal not available" in result["error"]
|
|
|
|
|
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
|
|
|
|
class TestWriteFileHandler:
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
2026-02-26 00:53:57 -08:00
|
|
|
def test_writes_content(self, mock_get):
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
2026-02-26 00:53:57 -08:00
|
|
|
result_obj.to_dict.return_value = {"status": "ok", "path": "/tmp/out.txt", "bytes": 13}
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
mock_ops.write_file.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import write_file_tool
|
2026-02-26 00:53:57 -08:00
|
|
|
result = json.loads(write_file_tool("/tmp/out.txt", "hello world!\n"))
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert result["status"] == "ok"
|
2026-02-26 00:53:57 -08:00
|
|
|
mock_ops.write_file.assert_called_once_with("/tmp/out.txt", "hello world!\n")
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_exception_returns_error_json(self, mock_get):
|
|
|
|
|
mock_get.side_effect = PermissionError("read-only filesystem")
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import write_file_tool
|
|
|
|
|
result = json.loads(write_file_tool("/tmp/out.txt", "data"))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
assert "read-only" in result["error"]
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestPatchHandler:
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
2026-02-26 00:53:57 -08:00
|
|
|
def test_replace_mode_calls_patch_replace(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"status": "ok", "replacements": 1}
|
|
|
|
|
mock_ops.patch_replace.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import patch_tool
|
|
|
|
|
result = json.loads(patch_tool(
|
|
|
|
|
mode="replace", path="/tmp/f.py",
|
|
|
|
|
old_string="foo", new_string="bar"
|
|
|
|
|
))
|
|
|
|
|
assert result["status"] == "ok"
|
|
|
|
|
mock_ops.patch_replace.assert_called_once_with("/tmp/f.py", "foo", "bar", False)
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_replace_mode_replace_all_flag(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"status": "ok", "replacements": 5}
|
|
|
|
|
mock_ops.patch_replace.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
from tools.file_tools import patch_tool
|
2026-02-26 00:53:57 -08:00
|
|
|
patch_tool(mode="replace", path="/tmp/f.py",
|
|
|
|
|
old_string="x", new_string="y", replace_all=True)
|
|
|
|
|
mock_ops.patch_replace.assert_called_once_with("/tmp/f.py", "x", "y", True)
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_replace_mode_missing_path_errors(self, mock_get):
|
|
|
|
|
from tools.file_tools import patch_tool
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
result = json.loads(patch_tool(mode="replace", path=None, old_string="a", new_string="b"))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_replace_mode_missing_strings_errors(self, mock_get):
|
|
|
|
|
from tools.file_tools import patch_tool
|
|
|
|
|
result = json.loads(patch_tool(mode="replace", path="/tmp/f.py", old_string=None, new_string="b"))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_patch_mode_calls_patch_v4a(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"status": "ok", "operations": 1}
|
|
|
|
|
mock_ops.patch_v4a.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import patch_tool
|
|
|
|
|
result = json.loads(patch_tool(mode="patch", patch="*** Begin Patch\n..."))
|
|
|
|
|
assert result["status"] == "ok"
|
|
|
|
|
mock_ops.patch_v4a.assert_called_once()
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_patch_mode_missing_content_errors(self, mock_get):
|
|
|
|
|
from tools.file_tools import patch_tool
|
|
|
|
|
result = json.loads(patch_tool(mode="patch", patch=None))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_unknown_mode_errors(self, mock_get):
|
|
|
|
|
from tools.file_tools import patch_tool
|
2026-02-26 00:53:57 -08:00
|
|
|
result = json.loads(patch_tool(mode="invalid_mode"))
|
|
|
|
|
assert "error" in result
|
|
|
|
|
assert "Unknown mode" in result["error"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSearchHandler:
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_search_calls_file_ops(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"matches": ["file1.py:3:match"]}
|
|
|
|
|
mock_ops.search.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import search_tool
|
|
|
|
|
result = json.loads(search_tool(pattern="TODO", target="content", path="."))
|
|
|
|
|
assert "matches" in result
|
|
|
|
|
mock_ops.search.assert_called_once()
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_search_passes_all_params(self, mock_get):
|
|
|
|
|
mock_ops = MagicMock()
|
|
|
|
|
result_obj = MagicMock()
|
|
|
|
|
result_obj.to_dict.return_value = {"matches": []}
|
|
|
|
|
mock_ops.search.return_value = result_obj
|
|
|
|
|
mock_get.return_value = mock_ops
|
|
|
|
|
|
|
|
|
|
from tools.file_tools import search_tool
|
|
|
|
|
search_tool(pattern="class", target="files", path="/src",
|
|
|
|
|
file_glob="*.py", limit=10, offset=5, output_mode="count", context=2)
|
|
|
|
|
mock_ops.search.assert_called_once_with(
|
|
|
|
|
pattern="class", path="/src", target="files", file_glob="*.py",
|
|
|
|
|
limit=10, offset=5, output_mode="count", context=2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@patch("tools.file_tools._get_file_ops")
|
|
|
|
|
def test_search_exception_returns_error(self, mock_get):
|
|
|
|
|
mock_get.side_effect = RuntimeError("no terminal")
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
|
2026-02-26 00:53:57 -08:00
|
|
|
from tools.file_tools import search_tool
|
|
|
|
|
result = json.loads(search_tool(pattern="x"))
|
test: reorganize test structure and add missing unit tests
Reorganize flat tests/ directory to mirror source code structure
(tools/, gateway/, hermes_cli/, integration/). Add 11 new test files
covering previously untested modules: registry, patch_parser,
fuzzy_match, todo_tool, approval, file_tools, gateway session/config/
delivery, and hermes_cli config/models. Total: 147 unit tests passing,
9 integration tests gated behind pytest marker.
2026-02-26 03:20:08 +03:00
|
|
|
assert "error" in result
|