Files
hermes-agent/tests/hermes_cli/test_cli_file_drop.py
teknium1 d10bb2ab6f test: make tests/ mirror the source tree; drop issue numbers from filenames
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.

Parallel directories for one source package, folded into the mirror:
  tests/acp        -> tests/acp_adapter   (its __init__/conftest move with it)
  tests/cli        -> tests/hermes_cli    (prompt_toolkit fixture merged into
                                           hermes_cli/conftest.py)
  tests/run_agent  -> tests/agent         (backoff fixture becomes
                                           agent/conftest.py)
  tests/relay      -> tests/gateway/relay
  tests/state      -> tests/hermes_state

246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.

Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.

Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).

Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
2026-09-13 09:18:02 -07:00

207 lines
7.6 KiB
Python

"""Tests for _detect_file_drop — file path detection that prevents
dragged/pasted absolute paths from being mistaken for slash commands."""
import pytest
from cli import _detect_file_drop
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def tmp_image(tmp_path):
"""Create a temporary .png file and return its path."""
img = tmp_path / "screenshot.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n") # minimal PNG header
return img
@pytest.fixture()
def tmp_text(tmp_path):
"""Create a temporary .py file and return its path."""
f = tmp_path / "main.py"
f.write_text("print('hello')\n")
return f
@pytest.fixture()
def tmp_image_with_spaces(tmp_path):
"""Create a file whose name contains spaces (like macOS screenshots)."""
img = tmp_path / "Screenshot 2026-04-01 at 7.25.32 PM.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n")
return img
# ---------------------------------------------------------------------------
# Tests: returns None for non-file inputs
# ---------------------------------------------------------------------------
class TestNonFileInputs:
def test_regular_slash_command(self):
assert _detect_file_drop("/help") is None
def test_empty_string(self):
assert _detect_file_drop("") is None
def test_nonexistent_path(self):
assert _detect_file_drop("/nonexistent/path/to/file.png") is None
def test_long_slash_command_does_not_raise(self):
"""Regression: long pasted slash commands like `/goal <long prose>`
used to raise OSError(ENAMETOOLONG, errno 63 macOS / 36 Linux)
from `Path.exists()` inside `_resolve_attachment_path`, which
propagated up to `process_loop`'s catch-all and silently lost
the user's input. The fix wraps the stat call in a try/except
OSError and returns None, letting the slash-command dispatch
path handle the input downstream.
Reproducer: paste a `/goal` followed by ~430 chars of prose.
Without the fix this triggers ENAMETOOLONG; with the fix it
cleanly returns None (file-drop = no), so `_looks_like_slash_command`
gets a chance to dispatch it.
"""
# 430-char `/goal` payload — well above NAME_MAX (255 bytes) on
# all common filesystems.
long_goal = (
"/goal " + ("Drive the board: triage triage-status items, "
"unblock spillover tasks where work is shipped, "
"advance P1 items by decomposing where needed. ") * 4
)
assert len(long_goal) > 255 # confirms it would have triggered ENAMETOOLONG
assert _detect_file_drop(long_goal) is None
# ---------------------------------------------------------------------------
# Tests: image file detection
# ---------------------------------------------------------------------------
class TestImageFileDrop:
def test_simple_image_path(self, tmp_image):
result = _detect_file_drop(str(tmp_image))
assert result is not None
assert result["path"] == tmp_image
assert result["is_image"] is True
assert result["remainder"] == ""
@pytest.mark.parametrize("ext", [".png", ".jpg", ".jpeg", ".gif", ".webp",
".bmp", ".tiff", ".tif", ".svg", ".ico"])
def test_all_image_extensions(self, tmp_path, ext):
img = tmp_path / f"test{ext}"
img.write_bytes(b"fake")
result = _detect_file_drop(str(img))
assert result is not None
assert result["is_image"] is True
# ---------------------------------------------------------------------------
# Tests: non-image file detection
# ---------------------------------------------------------------------------
class TestNonImageFileDrop:
def test_python_file(self, tmp_text):
result = _detect_file_drop(str(tmp_text))
assert result is not None
assert result["path"] == tmp_text
assert result["is_image"] is False
assert result["remainder"] == ""
# ---------------------------------------------------------------------------
# Tests: backslash-escaped spaces (macOS drag-and-drop)
# ---------------------------------------------------------------------------
class TestEscapedSpaces:
def test_escaped_spaces_in_path(self, tmp_image_with_spaces):
r"""macOS drags produce paths like /path/to/my\ file.png"""
escaped = str(tmp_image_with_spaces).replace(' ', '\\ ')
result = _detect_file_drop(escaped)
assert result is not None
assert result["path"] == tmp_image_with_spaces
assert result["is_image"] is True
def test_unquoted_spaces_in_path(self, tmp_image_with_spaces):
result = _detect_file_drop(str(tmp_image_with_spaces))
assert result is not None
assert result["path"] == tmp_image_with_spaces
assert result["is_image"] is True
assert result["remainder"] == ""
def test_mixed_escaped_and_literal_spaces_in_path(self, tmp_path):
img = tmp_path / "Screenshot 2026-04-21 at 1.04.43 PM.png"
img.write_bytes(b"\x89PNG\r\n\x1a\n")
mixed = str(img).replace("Screenshot ", "Screenshot\\ ").replace("2026-04-21 ", "2026-04-21\\ ").replace("at ", "at\\ ")
result = _detect_file_drop(mixed)
assert result is not None
assert result["path"] == img
assert result["is_image"] is True
assert result["remainder"] == ""
def test_tilde_prefixed_path(self, tmp_path, monkeypatch):
home = tmp_path / "home"
img = home / "storage" / "shared" / "Pictures" / "cat.png"
img.parent.mkdir(parents=True, exist_ok=True)
img.write_bytes(b"\x89PNG\r\n\x1a\n")
monkeypatch.setenv("HOME", str(home))
# ntpath.expanduser ignores HOME (Python 3.8+) — it wants USERPROFILE.
monkeypatch.setenv("USERPROFILE", str(home))
result = _detect_file_drop("~/storage/shared/Pictures/cat.png what is this?")
assert result is not None
assert result["path"] == img
assert result["is_image"] is True
assert result["remainder"] == "what is this?"
# ``windows_only`` rather than ``skipif(os.name != "nt")``: the Windows CI
# job selects ``-m windows_only``, so a bare skipif would leave this
# skipped on Linux AND unselected there — dead on every host.
@pytest.mark.windows_only
def test_windows_drive_letter_file_uri_drops_url_leading_slash(self, tmp_path):
image = tmp_path / "drive-uri.png"
image.write_bytes(b"\x89PNG\r\n\x1a\n")
uri = image.as_uri()
assert uri.startswith("file:///") and ":/" in uri
result = _detect_file_drop(uri)
assert result is not None
assert result["path"] == image
# ---------------------------------------------------------------------------
# Tests: edge cases
# ---------------------------------------------------------------------------
class TestEdgeCases:
def test_path_with_no_extension(self, tmp_path):
f = tmp_path / "Makefile"
f.write_text("all:\n\techo hi\n")
result = _detect_file_drop(str(f))
assert result is not None
assert result["is_image"] is False
def test_path_that_looks_like_command_but_is_file(self, tmp_path):
"""A file literally named 'help' inside a directory starting with /."""
f = tmp_path / "help"
f.write_text("not a command\n")
result = _detect_file_drop(str(f))
assert result is not None
assert result["is_image"] is False