mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(gateway): treat a leading @/ as a separator, not just an absolute path
`@/Desktop` returned nothing while `@Desktop` worked. The leading slash was always read literally, so the lookup went to the absolute `/Desktop` — which doesn't exist — and dead-ended instead of finding the folder one level down. The `@` has already announced "this is a path", so the slash people type next reads as a separator out of habit rather than a filesystem root. Take the absolute meaning only when it resolves: the parent directory has to exist, and a partially-typed segment has to match something in it. Otherwise drop the slash and resolve from the cwd. Real absolute paths are unaffected — `/usr`, `/etc/hos`, `/Users/...` all pass the existence probe and keep their current behaviour. The guard matters: stripping the slash unconditionally would let a repo-local `etc/` shadow the real `/etc`.
This commit is contained in:
parent
71e7eb3c16
commit
044cf46a0d
2 changed files with 102 additions and 0 deletions
|
|
@ -353,3 +353,65 @@ def test_fuzzy_finds_top_level_entries_outside_a_git_repo(tmp_path, monkeypatch)
|
|||
(tmp_path / "Desktop" / "note.txt").write_text("x")
|
||||
|
||||
assert "@folder:Desktop/" in [t for t, _, _ in _items("@Desktop")]
|
||||
|
||||
|
||||
# ── Leading slash is a separator, not necessarily an absolute path ───────
|
||||
# `@/Desktop` used to dead-end: it was read as the absolute `/Desktop`,
|
||||
# which doesn't exist. People type the slash out of habit — the `@` already
|
||||
# announced "this is a path" — so it should mean the same as `@Desktop`
|
||||
# unless a real absolute path is there.
|
||||
|
||||
|
||||
def test_leading_slash_matches_the_bare_form(tmp_path, monkeypatch):
|
||||
"""`@/foo` and `@foo` return the same thing when `/foo` doesn't exist."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "Desktop").mkdir()
|
||||
(tmp_path / "Desktop" / "note.txt").write_text("x")
|
||||
|
||||
server._fuzzy_cache.clear()
|
||||
bare = [t for t, _, _ in _items("@Desktop")]
|
||||
server._fuzzy_cache.clear()
|
||||
slashed = [t for t, _, _ in _items("@/Desktop")]
|
||||
|
||||
assert "@folder:Desktop/" in bare
|
||||
assert slashed == bare
|
||||
|
||||
|
||||
def test_leading_slash_navigates_into_subfolders(tmp_path, monkeypatch):
|
||||
"""The fallback survives deeper paths, not just a single segment."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "apps" / "desktop").mkdir(parents=True)
|
||||
(tmp_path / "apps" / "desktop" / "main.tsx").write_text("x")
|
||||
|
||||
assert "@folder:apps/desktop/" in [t for t, _, _ in _items("@/apps/desktop")]
|
||||
|
||||
server._fuzzy_cache.clear()
|
||||
assert any("main.tsx" in t for t, _, _ in _items("@/apps/desktop/"))
|
||||
|
||||
|
||||
def test_leading_slash_prefers_a_real_absolute_path(tmp_path, monkeypatch):
|
||||
"""When the absolute reading resolves, it wins — no silent rewrite.
|
||||
|
||||
A cwd-relative `usr/` must not shadow the real `/usr`, or typing an
|
||||
absolute path in a repo that happens to mirror those names breaks.
|
||||
"""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
# A decoy that would win if the slash were stripped unconditionally.
|
||||
(tmp_path / "etc").mkdir()
|
||||
(tmp_path / "etc" / "decoy.conf").write_text("x")
|
||||
|
||||
texts = [t for t, _, _ in _items("@/etc/")]
|
||||
|
||||
# `/etc` exists on any POSIX box, so the absolute reading must hold.
|
||||
assert not any("decoy.conf" in t for t in texts), texts
|
||||
|
||||
|
||||
def test_leading_slash_falls_back_only_when_absolute_is_missing(tmp_path, monkeypatch):
|
||||
"""A nonexistent absolute prefix falls back; an existing one doesn't."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "nonexistent-at-root").mkdir()
|
||||
(tmp_path / "nonexistent-at-root" / "f.txt").write_text("x")
|
||||
|
||||
assert "@folder:nonexistent-at-root/" in [
|
||||
t for t, _, _ in _items("@/nonexistent-at-root")
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16136,6 +16136,31 @@ def _fuzzy_basename_rank(name: str, query: str) -> tuple[int, int] | None:
|
|||
return None
|
||||
|
||||
|
||||
def _abs_completion_prefix_exists(path_part: str) -> bool:
|
||||
"""True when ``path_part`` reads sensibly as an absolute path.
|
||||
|
||||
A leading `/` is only meant literally if something is actually there:
|
||||
the parent directory has to exist, and a partially-typed final segment
|
||||
has to match at least one of its entries. Used to decide whether
|
||||
`@/foo` is the absolute `/foo` or shorthand for `foo` under the cwd.
|
||||
"""
|
||||
expanded = _normalize_completion_path(path_part)
|
||||
parent = os.path.dirname(expanded.rstrip("/")) or "/"
|
||||
tail = os.path.basename(expanded.rstrip("/"))
|
||||
|
||||
if not os.path.isdir(parent):
|
||||
return False
|
||||
|
||||
if not tail or expanded.endswith("/"):
|
||||
return os.path.isdir(expanded) or expanded == "/"
|
||||
|
||||
try:
|
||||
tail_lower = tail.lower()
|
||||
return any(e.lower().startswith(tail_lower) for e in os.listdir(parent))
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
@method("complete.path")
|
||||
def _(rid, params: dict) -> dict:
|
||||
word = params.get("word", "")
|
||||
|
|
@ -16171,6 +16196,21 @@ def _(rid, params: dict) -> dict:
|
|||
prefix_tag = ""
|
||||
path_part = query if is_context else query
|
||||
|
||||
# `@/foo` almost always means "foo, from here" rather than the absolute
|
||||
# `/foo`: the `@` already says "this is a path", so the slash reads as a
|
||||
# separator people type out of habit. Take the absolute reading only
|
||||
# when something is actually there, else drop the slash and resolve
|
||||
# relative to the cwd — otherwise `@/Desktop` dead-ends on a directory
|
||||
# that exists one level down. Real absolute paths (`@/usr/local`,
|
||||
# `@/etc/hosts`) still resolve, since those prefixes do exist.
|
||||
if (
|
||||
is_context
|
||||
and path_part.startswith("/")
|
||||
and not path_part.startswith("//")
|
||||
and not _abs_completion_prefix_exists(path_part)
|
||||
):
|
||||
path_part = path_part.lstrip("/")
|
||||
|
||||
# Fuzzy basename search across the repo when the user types a bare
|
||||
# name with no path separator — `@appChrome` surfaces every file
|
||||
# whose basename matches, regardless of directory depth. Matches what
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue