Files
hermes-webui/tests/test_issue4164_bound_non_git_project_context_walk.py
44ead2a73d fix(context): bound non-git project-context file walk at the workspace (#4164) (#4231)
* fix(#4164): bound project-context file walk in non-git workspaces

_project_context_candidates mirrored the agent's _find_hermes_md exactly,
including the bug that 'stop_at = git_root' is None in a non-git tree —
so the walk 'for directory in [cwd, *cwd.parents]: ... if stop_at and
directory == stop_at: break' never broke and ran to filesystem root.
A workspace at /tmp/x/project/subdir could surface /tmp/x/HERMES.md
(a file outside the workspace) in the Memory > Project Context tab.

Fix: when no git root is found, treat the workspace itself as the stop
boundary. The cwd is still scanned (preserving in-workspace AGENTS.md /
HERMES.md behavior), but parents are skipped — we never walk into the
user's home directory or /tmp.

Git workspaces are unchanged (still walk up to the git root). The
existing test_3866 walk-to-git-root test still passes.

The issue notes the agent-side walk in agent/prompt_builder._find_hermes_md
should be bounded the same way for full parity; that lives in a separate
repo (hermes-agent), so this PR fixes only the WebUI side. Until the
agent ships the same bound, the WebUI tab will under-report context
files that live above a non-git workspace — strictly less surprising
than the previous over-report.

tests/test_issue4164_bound_non_git_project_context_walk.py (4 tests) pins:
- non-git workspace does not leak a parent HERMES.md
- non-git workspace still reads in-workspace context files
- git workspace walk-to-git-root is unchanged
- workspace-level HERMES.md wins, parent copy doesn't appear even shadowed

All 10 existing test_3866_project_context_memory_section tests still pass.

Closes #4164.

* chore(release): stamp v0.51.428 (Release OO) for #4164

---------

Co-authored-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 23:22:29 -07:00

104 lines
4.1 KiB
Python

"""Regression for #4164: bound the project-context file walk in non-git workspaces.
Before this fix, ``_project_context_candidates`` mirrored the agent's
``_find_hermes_md`` exactly — including the bug that ``stop_at = git_root``
is ``None`` in a non-git tree, which caused the ``for directory in [cwd,
*cwd.parents]: ... if stop_at and directory == stop_at: break`` walk to
never break and run all the way to filesystem root. A workspace at
``/tmp/x/project/subdir`` could then surface ``/tmp/x/HERMES.md`` (a file
*outside* the user's workspace) in the Project Context tab.
The fix bounds the walk at the workspace itself when no git root is found:
the cwd is still scanned, but parents are skipped. This is the minimal,
non-breaking bound — git workspaces are unchanged, and in-workspace files
still load.
"""
from __future__ import annotations
import pathlib
import api.routes as routes
def _ctx(workspace):
return routes._read_active_project_context(pathlib.Path(workspace))
def test_non_git_workspace_does_not_walk_above_workspace(tmp_path):
"""Sentinel HERMES.md placed *above* the workspace in a non-git tree must
not be surfaced — the pre-fix walk would have loaded it because stop_at
was None and the loop ran to filesystem root."""
above = tmp_path / "outside"
above.mkdir()
(above / "HERMES.md").write_text("LEAKED FROM OUTSIDE", encoding="utf-8")
workspace = above / "project" / "subdir"
workspace.mkdir(parents=True)
# Intentionally no .git anywhere in the tree, and no HERMES/AGENTS file
# inside the workspace — so any non-empty result means we walked above.
data = _ctx(workspace)
assert "LEAKED FROM OUTSIDE" not in data["content"], (
"Non-git workspace walk leaked an HERMES.md above the workspace root"
)
assert data["path"] == "", (
f"Expected empty path (no in-workspace context file); got {data['path']!r}"
)
def test_non_git_workspace_still_reads_in_workspace_context(tmp_path):
"""The bound must only block *parents* — files inside the workspace itself
must still load exactly as before."""
workspace = tmp_path / "no-git-here"
workspace.mkdir()
(workspace / "AGENTS.md").write_text("# In-workspace rules", encoding="utf-8")
data = _ctx(workspace)
assert "In-workspace rules" in data["content"]
assert data["path"].endswith("AGENTS.md")
assert data["name"] == "AGENTS.md"
def test_git_workspace_walk_to_git_root_is_unchanged(tmp_path):
"""Git workspaces must keep walking up to the git root — the bound only
activates when git_root is None. This duplicates the pre-existing
``test_project_context_walks_hermes_md_to_git_root_but_not_agents_md``
intent specifically against the #4164 patch site."""
root = tmp_path / "repo"
nested = root / "src" / "deep" / "pkg"
nested.mkdir(parents=True)
(root / ".git").mkdir()
(root / "HERMES.md").write_text("# Root-of-repo context", encoding="utf-8")
data = _ctx(nested)
assert "Root-of-repo context" in data["content"]
assert data["path"].endswith("HERMES.md")
def test_non_git_workspace_bound_is_workspace_not_first_parent(tmp_path):
"""Edge case: if the workspace itself contains a HERMES.md AND a parent
also contains one, the workspace copy must win and the parent copy must
NOT leak in via the candidate list (i.e. the bound triggers immediately,
not after one extra parent step)."""
parent = tmp_path / "container"
parent.mkdir()
(parent / "HERMES.md").write_text("PARENT COPY", encoding="utf-8")
workspace = parent / "ws"
workspace.mkdir()
(workspace / "HERMES.md").write_text("WORKSPACE COPY", encoding="utf-8")
data = _ctx(workspace)
assert "WORKSPACE COPY" in data["content"]
assert "PARENT COPY" not in data["content"]
# And the parent copy must NOT appear as a "shadowed" hit either —
# it's not just lower priority, it's out of scope entirely.
assert all("PARENT COPY" not in (s.get("path") or "") for s in data["shadowed"])
assert not any(
str(parent / "HERMES.md") == s.get("path") for s in data["shadowed"]
)