mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Systematic prune per AGENTS.md test policy, one pass over every major test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli, cron, tui_gateway, honcho/openviking, root-level): - DELETE: source-reading tests (read_text/getsource on prod files), change-detector tests (exact catalog counts, model-name snapshots, config version literals), mock-echo tests (assert a mock returns what it was told), assertion-free/trivial tests, near-duplicate parametrizations (boundaries + one representative kept), async/sync twin duplicates, cosmetic within-file variations. - KEEP (mandatory): security/redaction/approval guards, message-role alternation invariants, prompt-caching/deterministic-call-id invariants, issue-number regression tests (deduped), E2E tests. - 6 test files deleted outright (script-style/no-assert or fully redundant); conftest.py, fakes/, fixtures/ untouched. - tests/acp/conftest.py added: autouse fixture stubs the live models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server tests performed on every session create — test_server.py 147s → 3.4s, and the tests are now genuinely hermetic. - Sleep-based slowness shrunk where safe (codex_ttfb_watchdog, compression_concurrent_fork, etc.); no wall-clock assertion tightened. Verification: full hermetic suite via scripts/run_tests.sh — 2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall (baseline: 583s wall, 13,564s subprocess CPU).
224 lines
8 KiB
Python
224 lines
8 KiB
Python
"""
|
|
Tests for extract_local_files() — auto-detection of bare local file paths
|
|
in model response text for native media delivery.
|
|
|
|
Covers: path matching, code-block exclusion, URL rejection, tilde expansion,
|
|
deduplication, text cleanup, and extension routing.
|
|
|
|
Based on PR #1636 by sudoingX (salvaged + hardened).
|
|
"""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from gateway.platforms.base import BasePlatformAdapter
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _extract(content: str, existing_files: set[str] | None = None):
|
|
"""
|
|
Run extract_local_files with os.path.isfile mocked to return True
|
|
for any path in *existing_files* (expanded form). If *existing_files*
|
|
is None every path passes.
|
|
"""
|
|
existing = existing_files
|
|
|
|
def fake_isfile(p):
|
|
if existing is None:
|
|
return True
|
|
return p in existing
|
|
|
|
def fake_expanduser(p):
|
|
if p.startswith("~/"):
|
|
return "/home/user" + p[1:]
|
|
return p
|
|
|
|
with patch("os.path.isfile", side_effect=fake_isfile), \
|
|
patch("os.path.expanduser", side_effect=fake_expanduser):
|
|
return BasePlatformAdapter.extract_local_files(content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Basic detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBasicDetection:
|
|
|
|
def test_absolute_path_image(self):
|
|
paths, cleaned = _extract("Here is the screenshot /root/screenshots/game.png enjoy")
|
|
assert paths == ["/root/screenshots/game.png"]
|
|
assert "/root/screenshots/game.png" not in cleaned
|
|
assert "Here is the screenshot" in cleaned
|
|
|
|
def test_tilde_path_image(self):
|
|
paths, cleaned = _extract("Check out ~/photos/cat.jpg for the cat")
|
|
assert paths == ["/home/user/photos/cat.jpg"]
|
|
assert "~/photos/cat.jpg" not in cleaned
|
|
|
|
def test_video_extensions(self):
|
|
for ext in (".mp4", ".mov", ".avi", ".mkv", ".webm"):
|
|
text = f"Video at /tmp/clip{ext} here"
|
|
paths, _ = _extract(text)
|
|
assert len(paths) == 1, f"Failed for {ext}"
|
|
assert paths[0] == f"/tmp/clip{ext}"
|
|
|
|
def test_image_extensions(self):
|
|
for ext in (".png", ".jpg", ".jpeg", ".gif", ".webp"):
|
|
text = f"Image at /tmp/pic{ext} here"
|
|
paths, _ = _extract(text)
|
|
assert len(paths) == 1, f"Failed for {ext}"
|
|
assert paths[0] == f"/tmp/pic{ext}"
|
|
|
|
def test_document_extensions(self):
|
|
"""Documents (PDF, Word, plain text, etc.) ship as file uploads."""
|
|
for ext in (".pdf", ".docx", ".doc", ".odt", ".rtf", ".txt", ".md"):
|
|
text = f"Report at /tmp/report{ext} attached"
|
|
paths, _ = _extract(text)
|
|
assert len(paths) == 1, f"Failed for {ext}"
|
|
assert paths[0] == f"/tmp/report{ext}"
|
|
|
|
|
|
def test_path_at_line_start(self):
|
|
paths, _ = _extract("/var/data/image.png")
|
|
assert paths == ["/var/data/image.png"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Non-existent files are skipped
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsfileGuard:
|
|
|
|
def test_nonexistent_path_skipped(self):
|
|
"""Paths that don't exist on disk are not extracted."""
|
|
paths, cleaned = _extract(
|
|
"See /tmp/nope.png here",
|
|
existing_files=set(), # nothing exists
|
|
)
|
|
assert paths == []
|
|
assert "/tmp/nope.png" in cleaned # not stripped
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# URL false-positive prevention
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestURLRejection:
|
|
|
|
def test_https_url_not_matched(self):
|
|
"""Paths embedded in HTTP URLs must not be extracted."""
|
|
paths, cleaned = _extract("Visit https://example.com/images/photo.png for details")
|
|
# The regex lookbehind should prevent matching the URL's path segment
|
|
# Even if it did match, isfile would be False for /images/photo.png
|
|
# (we mock isfile to True-for-all here, so the lookbehind is the guard)
|
|
assert paths == []
|
|
assert "https://example.com/images/photo.png" in cleaned
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Code block exclusion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCodeBlockExclusion:
|
|
|
|
def test_fenced_code_block_skipped(self):
|
|
text = "Here's how:\n```python\nimg = open('/tmp/image.png')\n```\nDone."
|
|
paths, cleaned = _extract(text)
|
|
assert paths == []
|
|
assert "/tmp/image.png" in cleaned # not stripped
|
|
|
|
def test_inline_code_skipped(self):
|
|
text = "Use the path `/tmp/image.png` in your config"
|
|
paths, cleaned = _extract(text)
|
|
assert paths == []
|
|
assert "`/tmp/image.png`" in cleaned
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deduplication
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDeduplication:
|
|
|
|
def test_duplicate_paths_deduplicated(self):
|
|
text = "See /tmp/img.png and also /tmp/img.png again"
|
|
paths, _ = _extract(text)
|
|
assert paths == ["/tmp/img.png"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Text cleanup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestTextCleanup:
|
|
|
|
def test_path_removed_from_text(self):
|
|
paths, cleaned = _extract("Before /tmp/x.png after")
|
|
assert "Before" in cleaned
|
|
assert "after" in cleaned
|
|
assert "/tmp/x.png" not in cleaned
|
|
|
|
def test_excessive_blank_lines_collapsed(self):
|
|
text = "Before\n\n\n/tmp/x.png\n\n\nAfter"
|
|
_, cleaned = _extract(text)
|
|
assert "\n\n\n" not in cleaned
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestEdgeCases:
|
|
|
|
def test_empty_string(self):
|
|
paths, cleaned = _extract("")
|
|
assert paths == []
|
|
assert cleaned == ""
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"content,expected",
|
|
[
|
|
# Backslash separators (native Windows style)
|
|
("See C:\\Users\\test\\image.png here", "C:\\Users\\test\\image.png"),
|
|
# Forward slashes with drive letter (common in cross-platform code)
|
|
("See C:/Users/test/image.png here", "C:/Users/test/image.png"),
|
|
# Non-C: drive
|
|
("Video at D:/data/clip.mp4 ready", "D:/data/clip.mp4"),
|
|
# Lowercase drive letter
|
|
("Path e:/audio/track.mp3 done", "e:/audio/track.mp3"),
|
|
],
|
|
)
|
|
def test_windows_drive_letter_paths_matched(self, content, expected):
|
|
"""Windows drive-letter paths (C:/..., C:\\...) must be detected (#34632).
|
|
|
|
Prior behavior anchored on (?:~/|/) only, which silently dropped
|
|
Windows absolute paths so the agent's bare-path references were
|
|
sent as text instead of native uploads.
|
|
"""
|
|
paths, cleaned = _extract(content)
|
|
assert paths == [expected]
|
|
assert expected not in cleaned
|
|
|
|
def test_relative_windows_path_not_matched(self):
|
|
"""A bare Windows-style filename without a drive letter must still
|
|
not match (e.g. ``foo\\bar.png`` is treated as relative, like its
|
|
Unix sibling ``foo/bar.png``)."""
|
|
paths, _ = _extract("File at foo\\bar.png here")
|
|
assert paths == []
|
|
|
|
|
|
def test_path_followed_by_punctuation(self):
|
|
"""Path followed by comma, period, paren should still match."""
|
|
for suffix in [",", ".", ")", ":", ";"]:
|
|
text = f"See /tmp/img.png{suffix} details"
|
|
paths, _ = _extract(text)
|
|
assert len(paths) == 1, f"Failed with suffix '{suffix}'"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|