Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions agent/subdirectory_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _add_path_candidate(self, raw_path: str, candidates: Set[Path]):
if parent == p:
break # filesystem root
p = parent
except (OSError, ValueError):
except (OSError, ValueError, RuntimeError):
pass

def _extract_paths_from_command(self, cmd: str, candidates: Set[Path]):
Expand Down Expand Up @@ -188,7 +188,7 @@ def _is_valid_subdir(self, path: Path) -> bool:
try:
if not path.is_relative_to(self.working_dir):
return False
except (OSError, ValueError):
except (OSError, ValueError, RuntimeError):
# Older Python or path resolution error β€” fall back to parent
# check as a best-effort safeguard.
if not _is_ancestor_or_same(self.working_dir, path):
Expand All @@ -210,7 +210,7 @@ def _load_hints_for_directory(self, directory: Path) -> Optional[str]:
directory, self.working_dir,
)
return None
except (OSError, ValueError):
except (OSError, ValueError, RuntimeError):
if not _is_ancestor_or_same(self.working_dir, directory):
logger.debug(
"Skipping hint files in %s β€” outside working_dir %s",
Expand Down Expand Up @@ -245,7 +245,7 @@ def _load_hints_for_directory(self, directory: Path) -> Optional[str]:
try:
rel_path = str(hint_path.relative_to(Path.home()))
rel_path = "~/" + rel_path
except ValueError:
except (ValueError, RuntimeError):
pass # keep absolute
found_hints.append((rel_path, content))
# First match wins per directory (like startup loading)
Expand Down
112 changes: 112 additions & 0 deletions tests/agent/test_subdirectory_hints_runtime_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Tests for agent.subdirectory_hints RuntimeError guards.

Python 3.11+ raises RuntimeError (not OSError/ValueError) from
Path.expanduser() and Path.home() when $HOME is unset. These tests
verify that the hint tracker handles the missing-$HOME case gracefully
instead of crashing the agent conversation loop.

Regression test for https://github.com/NousResearch/hermes-agent/issues/45401
"""

from __future__ import annotations

import os
from pathlib import Path
from unittest.mock import patch

import pytest

from agent.subdirectory_hints import SubdirectoryHintTracker


@pytest.fixture()
def tracker(tmp_path: Path) -> SubdirectoryHintTracker:
"""Create a tracker rooted in a temporary directory."""
return SubdirectoryHintTracker(working_dir=tmp_path)


# ---------------------------------------------------------------------------
# _add_path_candidate β€” expanduser() RuntimeError
# ---------------------------------------------------------------------------

class TestAddPathCandidateRuntimeError:
"""expanduser() raises RuntimeError when $HOME is unset (Python 3.11+)."""

def test_expanduser_runtime_error_gracefully_skipped(
self, tracker: SubdirectoryHintTracker, tmp_path: Path
) -> None:
"""_add_path_candidate must not crash when expanduser() raises RuntimeError."""
candidates: set[Path] = set()

with patch("pathlib.Path.expanduser", side_effect=RuntimeError("Could not determine home directory.")):
# Should not raise β€” RuntimeError is caught
tracker._add_path_candidate("~/some/path", candidates)

# Candidate set should be empty (path was skipped)
assert candidates == set()


# ---------------------------------------------------------------------------
# _is_valid_subdir β€” is_relative_to() RuntimeError
# ---------------------------------------------------------------------------

class TestIsValidSubdirRuntimeError:
"""is_relative_to() may raise RuntimeError on some Python builds."""

def test_is_relative_to_runtime_error_returns_false(
self, tracker: SubdirectoryHintTracker, tmp_path: Path
) -> None:
"""_is_valid_subdir must return False (not crash) when is_relative_to raises RuntimeError."""
# Use a path outside the working directory so _is_ancestor_or_same
# also returns False after the RuntimeError fallback.
outside = Path("/tmp/outside-workdir-subdir")
outside.mkdir(exist_ok=True)
try:
with patch.object(Path, "is_relative_to", side_effect=RuntimeError("Could not determine home directory.")):
result = tracker._is_valid_subdir(outside)
finally:
outside.rmdir()

assert result is False


# ---------------------------------------------------------------------------
# _load_hints_for_directory β€” is_relative_to() + Path.home() RuntimeError
# ---------------------------------------------------------------------------

class TestLoadHintsForDirectoryRuntimeError:
"""_load_hints_for_directory must not crash when $HOME is unset."""

def test_is_relative_to_runtime_error_returns_none(
self, tracker: SubdirectoryHintTracker, tmp_path: Path
) -> None:
"""When is_relative_to raises RuntimeError, returns None (skip directory)."""
subdir = tmp_path / "project"
subdir.mkdir()

with patch.object(Path, "is_relative_to", side_effect=RuntimeError("Could not determine home directory.")):
result = tracker._load_hints_for_directory(subdir)

assert result is None

def test_path_home_runtime_error_in_relative_path_fallback(
self, tracker: SubdirectoryHintTracker, tmp_path: Path
) -> None:
"""When Path.home() raises RuntimeError in the relative-path fallback, hint is still returned."""
subdir = tmp_path / "project"
subdir.mkdir()
hint_file = subdir / "AGENTS.md"
hint_file.write_text("# Test hint\n")

original_home = Path.home

def _no_home():
raise RuntimeError("Could not determine home directory.")

with patch.object(Path, "home", side_effect=_no_home):
result = tracker._load_hints_for_directory(subdir)

# Hint should still be loaded β€” the RuntimeError only affects the
# display path fallback, not the actual content.
assert result is not None
assert "Test hint" in result
Loading