Files
hermes-agent/tests/tools/test_file_operations_delete.py
kshitijk4poor 22facf4fdf fix(file_ops): a trailing separator must not empty the Delete/Move entry leaf
'.../.ssh/link/' split to an empty basename, so the entry check degenerated
to checking the link's TARGET: get_write_denied_error(entry=True) and
is_protected_path(follow=False) let the delete remove a link inside ~/.ssh,
and _resolve_entry_for_task fell back to full resolution, so a V4A
'*** Delete File: dir/link/' deleted the file the link points to (the
original bug, trailing-slash form).

split_entry() drops trailing separators (keeping a bare '/' or drive root)
before the parent/leaf split and is used by all three entry-mode sites.
is_protected_path(follow=False) now normcases the joined entry, not only
its parent, so a case-variant spelling of the exe/venv entry still matches
on Windows.
2026-09-27 01:10:56 +05:30

79 lines
2.8 KiB
Python

"""Behavior tests for ``ShellFileOperations.delete_file`` (real LocalEnvironment shell).
delete_file's python snippet contract: a regular file is unlinked; a directory
is refused (``is a directory``), never silently removed — no recursive delete
exists on this interface.
"""
import os
import sys
import pytest
from tools.environments.local import LocalEnvironment
from tools.file_operations import ShellFileOperations
@pytest.fixture()
def ops(tmp_path):
env = LocalEnvironment(cwd=str(tmp_path))
return ShellFileOperations(env, cwd=str(tmp_path))
def test_delete_file_removes_regular_file(ops, tmp_path):
target = tmp_path / "doomed.txt"
target.write_text("bye", encoding="utf-8")
result = ops.delete_file(str(target))
assert result.error is None
assert not target.exists()
def test_delete_file_refuses_directory(ops, tmp_path):
target = tmp_path / "dir"
target.mkdir()
(target / "keep.txt").write_text("still here", encoding="utf-8")
result = ops.delete_file(str(target))
assert result.error is not None
assert "is a directory" in result.error
assert target.exists()
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("op", ["delete", "delete_trailing_slash", "move"])
@pytest.mark.parametrize("layout", ["beside_runtime_venv", "link_in_credential_dir"])
def test_delete_and_move_guard_the_entry_itself(ops, tmp_path, monkeypatch, op, layout):
"""Delete/Move vet the directory entry (parent resolved, leaf kept): a plain file whose
directory merely CONTAINS the runtime venv (``~/notes.txt``) stays deletable/movable,
while a link directly inside a credential dir is refused even though it points outside.
A trailing separator (``.ssh/link/``) must not empty the leaf and skip the entry check."""
home, outside = tmp_path / "home", tmp_path / "outside.txt"
outside.write_text("keep", encoding="utf-8")
monkeypatch.setenv("HOME", str(home))
if layout == "beside_runtime_venv":
(home / ".venv").mkdir(parents=True)
monkeypatch.setattr(sys, "prefix", str(home / ".venv"))
entry = home / "plain.txt"
entry.write_text("x", encoding="utf-8")
else:
(home / ".ssh").mkdir(parents=True)
entry = home / ".ssh" / "link"
entry.symlink_to(outside)
moved = home / "moved.txt"
if op == "move":
result = ops.move_file(str(entry), str(moved))
else:
result = ops.delete_file(str(entry) + ("/" if op == "delete_trailing_slash" else ""))
if layout == "beside_runtime_venv":
assert result.error is None, result.error
assert not os.path.lexists(entry) and moved.exists() == (op == "move")
else:
assert result.error and "protected" in result.error, result.error
assert entry.is_symlink() and not os.path.lexists(moved)
assert outside.read_text(encoding="utf-8-sig") == "keep"