fix(tui): a real named profile "hermes" is not swallowed by the legacy-basename alias
"hermes" matches the profile-id regex, so canonicalising it unconditionally at the RPC boundary misrouted a genuine <root>/profiles/hermes to the default profile. Alias only when no such named profile exists; ".hermes" can never be a real id and stays aliased. Also: profile_name_for_home collapses its duplicated pre/post-resolve block into one loop over (path, resolved path) and drops the bare "parent named profiles" fallback that bypassed named_profile_home's root check; _profile_home goes back to main's single resolve() comparison; the symlink-loop assertion in the target-unavailable test is no longer wrapped in a try/except that could silently skip it.
This commit is contained in:
+4
-13
@@ -217,23 +217,14 @@ def profile_name_for_home(path: str | Path | None) -> str | None:
|
||||
current = Path(path).expanduser()
|
||||
try:
|
||||
default_root = get_default_hermes_root()
|
||||
if current == default_root:
|
||||
return "default"
|
||||
if current.parent.name == "profiles":
|
||||
named = named_profile_home(current)
|
||||
for candidate in (current, current.resolve(strict=False)):
|
||||
if candidate == default_root or candidate == default_root.resolve(strict=False):
|
||||
return "default"
|
||||
named = named_profile_home(candidate)
|
||||
if named is not None:
|
||||
return named.name
|
||||
if current.name and not current.name.startswith("."):
|
||||
return current.name
|
||||
if current.resolve(strict=False) == default_root.resolve(strict=False):
|
||||
return "default"
|
||||
named = named_profile_home(current)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
return None
|
||||
if named is not None:
|
||||
return named.name
|
||||
if current.parent.name == "profiles" and current.name and not current.name.startswith("."):
|
||||
return current.name
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ def test_default_home_aliases_are_reported_as_default(tmp_path, monkeypatch):
|
||||
for alias in (default_home.name, "hermes"):
|
||||
assert server._response_profile_name(alias) == "default"
|
||||
assert server._session_info(None, {"profile_home": str(default_home)})["profile_name"] == "default"
|
||||
# "hermes" is a legal profile id: a REAL named profile of that name is never swallowed by the alias.
|
||||
(default_home / "profiles" / "hermes").mkdir(parents=True)
|
||||
assert server._response_profile_name("hermes") == "hermes"
|
||||
|
||||
|
||||
def test_profile_home_resolution_stamps_default_rows(tmp_path, monkeypatch):
|
||||
@@ -165,4 +168,3 @@ def test_custom_default_root_real_session_db_owner_stamping(tmp_path, monkeypatc
|
||||
assert launch_db.get_session("lazy-default") is None
|
||||
assert launch_db.get_session("seeded-default") is None
|
||||
assert launch_db.get_session("branched-default") is None
|
||||
|
||||
|
||||
@@ -35,13 +35,9 @@ def test_explicit_profile_target_never_falls_back(tmp_path, monkeypatch):
|
||||
# A real resolution I/O failure must propagate, too (no predicate patch).
|
||||
profiles = home / "profiles"
|
||||
profiles.rename(home / "saved-profiles")
|
||||
try:
|
||||
profiles.symlink_to("profiles")
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
with pytest.raises((OSError, RuntimeError)):
|
||||
server._profile_home("worker")
|
||||
profiles.symlink_to("profiles")
|
||||
with pytest.raises((OSError, RuntimeError)):
|
||||
server._profile_home("worker")
|
||||
|
||||
|
||||
def test_custom_root_basename_target_fails_closed_when_unavailable(tmp_path, monkeypatch):
|
||||
@@ -61,4 +57,3 @@ def test_custom_root_basename_target_fails_closed_when_unavailable(tmp_path, mon
|
||||
with pytest.raises(FileNotFoundError):
|
||||
with server._profile_db({"profile": "customer-data"}):
|
||||
pass
|
||||
|
||||
|
||||
@@ -448,12 +448,14 @@ def _profile_db(params: dict | None = None):
|
||||
def _canonical_profile_request(name: str) -> str:
|
||||
"""Canonicalize profile basenames emitted by older session-info payloads.
|
||||
|
||||
``Path(default_home).name`` was historically sent as a profile id. Those
|
||||
basenames are installation details, while explicit unknown profile names
|
||||
must continue to fail closed in ``_profile_home``.
|
||||
``Path(default_home).name`` was historically sent as a profile id. Those basenames are
|
||||
installation details — unless a real named profile of that name exists (``hermes`` is a legal
|
||||
id), in which case it wins; other unknown names keep failing closed in ``_profile_home``.
|
||||
"""
|
||||
if name.casefold() in {".hermes", "hermes"}:
|
||||
return "default"
|
||||
from hermes_cli import profiles as profiles_mod
|
||||
if not Path(profiles_mod.get_profile_dir(name)).is_dir():
|
||||
return "default"
|
||||
return name
|
||||
|
||||
|
||||
@@ -478,12 +480,8 @@ def _profile_home(profile: str | None) -> Path | None:
|
||||
home = Path(profiles_mod.get_profile_dir(name))
|
||||
if not home.is_dir():
|
||||
raise FileNotFoundError(f"Profile '{name}' does not exist.")
|
||||
launch = Path(_hermes_home)
|
||||
if home == launch:
|
||||
if home.resolve() == Path(_hermes_home).resolve():
|
||||
return None # already the launch profile (no override needed)
|
||||
if home.name == launch.name or home.is_symlink() or launch.is_symlink():
|
||||
if home.resolve() == launch.resolve():
|
||||
return None
|
||||
_served_profile_homes.add(home) # the change watcher must stat every served sibling store too
|
||||
return home
|
||||
|
||||
|
||||
Reference in New Issue
Block a user