fix(profiles): migrate Honcho host on rename

This commit is contained in:
helix4u 2026-04-27 16:11:36 -06:00 committed by Teknium
parent c69310c625
commit d8c5573ffe
2 changed files with 121 additions and 2 deletions

View file

@ -954,6 +954,59 @@ def import_profile(archive_path: str, name: Optional[str] = None) -> Path:
# Rename
# ---------------------------------------------------------------------------
def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) -> None:
"""Rename Honcho host blocks for a renamed profile without changing peers."""
old_host = f"hermes.{old_name}"
new_host = f"hermes.{new_name}"
candidates = [
new_dir / "honcho.json",
_get_default_hermes_home() / "honcho.json",
Path.home() / ".honcho" / "config.json",
]
seen: set[Path] = set()
for path in candidates:
try:
resolved = path.resolve()
except OSError:
resolved = path
if resolved in seen or not path.is_file():
continue
seen.add(resolved)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
hosts = raw.get("hosts")
if not isinstance(hosts, dict) or old_host not in hosts:
continue
if new_host in hosts:
print(f"⚠ Honcho host block not migrated: {new_host} already exists in {path}")
continue
block = hosts[old_host]
if isinstance(block, dict) and "aiPeer" not in block:
bare = old_host.split(".", 1)[1] if "." in old_host else old_host
block["aiPeer"] = bare
hosts[new_host] = hosts.pop(old_host)
tmp = path.with_suffix(path.suffix + ".tmp")
try:
tmp.write_text(json.dumps(raw, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
tmp.replace(path)
except OSError:
try:
tmp.unlink(missing_ok=True)
except OSError:
pass
continue
print(f"✓ Honcho host updated: {old_host}{new_host}")
def rename_profile(old_name: str, new_name: str) -> Path:
"""Rename a profile: directory, wrapper script, service, active_profile.
@ -984,7 +1037,10 @@ def rename_profile(old_name: str, new_name: str) -> Path:
old_dir.rename(new_dir)
print(f"✓ Renamed {old_dir.name}{new_dir.name}")
# 3. Update wrapper script
# 3. Update profile-scoped Honcho host blocks, preserving aiPeer identity
_migrate_honcho_profile_host(old_name, new_name, new_dir)
# 4. Update wrapper script
remove_wrapper_script(old_name)
collision = check_alias_collision(new_name)
if not collision:
@ -993,7 +1049,7 @@ def rename_profile(old_name: str, new_name: str) -> Path:
else:
print(f"⚠ Cannot create alias '{new_name}'{collision}")
# 4. Update active_profile if it pointed to old name
# 5. Update active_profile if it pointed to old name
try:
if get_active_profile() == old_name:
set_active_profile(new_name)

View file

@ -384,6 +384,69 @@ class TestRenameProfile:
assert new_dir.is_dir()
assert new_dir == tmp_path / ".hermes" / "profiles" / "newname"
def test_renames_root_honcho_host_without_changing_ai_peer(self, profile_env):
tmp_path = profile_env
create_profile("ssi_health", no_alias=True)
honcho_path = tmp_path / ".hermes" / "honcho.json"
honcho_path.write_text(json.dumps({
"hosts": {
"hermes.ssi_health": {
"recallMode": "hybrid",
"writeFrequency": "async",
"sessionStrategy": "per-session",
"saveMessages": True,
"peerName": "user-peer",
"aiPeer": "ssi_health",
"workspace": "hermes",
"enabled": True,
}
}
}))
with patch("hermes_cli.profiles.check_alias_collision", return_value="skip"):
rename_profile("ssi_health", "heimdall")
cfg = json.loads(honcho_path.read_text())
assert "hermes.ssi_health" not in cfg["hosts"]
assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "ssi_health"
assert cfg["hosts"]["hermes.heimdall"]["peerName"] == "user-peer"
def test_pins_ai_peer_when_absent_on_honcho_host_rename(self, profile_env):
tmp_path = profile_env
create_profile("ssi_health", no_alias=True)
honcho_path = tmp_path / ".hermes" / "honcho.json"
honcho_path.write_text(json.dumps({
"hosts": {
"hermes.ssi_health": {"workspace": "hermes", "enabled": True}
}
}))
with patch("hermes_cli.profiles.check_alias_collision", return_value="skip"):
rename_profile("ssi_health", "heimdall")
cfg = json.loads(honcho_path.read_text())
assert "hermes.ssi_health" not in cfg["hosts"]
assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "ssi_health"
assert cfg["hosts"]["hermes.heimdall"]["workspace"] == "hermes"
def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):
tmp_path = profile_env
create_profile("ssi_health", no_alias=True)
honcho_path = tmp_path / ".hermes" / "honcho.json"
honcho_path.write_text(json.dumps({
"hosts": {
"hermes.ssi_health": {"aiPeer": "ssi_health"},
"hermes.heimdall": {"aiPeer": "heimdall"},
}
}))
with patch("hermes_cli.profiles.check_alias_collision", return_value="skip"):
rename_profile("ssi_health", "heimdall")
cfg = json.loads(honcho_path.read_text())
assert cfg["hosts"]["hermes.ssi_health"]["aiPeer"] == "ssi_health"
assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "heimdall"
def test_default_raises_value_error(self, profile_env):
with pytest.raises(ValueError, match="default"):
rename_profile("default", "newname")