Files
aiturk-hermes-ide/tests/hermes_cli/test_deleted_profile_tombstone.py
T

213 lines
8.8 KiB
Python

"""Deleted named profiles must stay gone until explicitly recreated.
A live serve/logging process can mkdir ``profiles/<name>/logs`` after
``hermes profile delete`` removes the tree. That empty shell then
reappears in ``hermes profile list`` and Desktop Bot Mode. These tests
lock the tombstone + no-mkdir contract without depending on Desktop.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from hermes_cli.config import ensure_hermes_home
from hermes_cli.profiles import (
backfill_profile_envs,
create_profile,
delete_profile,
list_profiles,
profile_exists,
profiles_to_serve,
resolve_profile_env,
set_active_profile,
)
from hermes_constants import named_profile_home
from hermes_logging import setup_logging
@pytest.fixture()
def profile_env(tmp_path, monkeypatch):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
default_home = tmp_path / ".hermes"
default_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(default_home))
return tmp_path
def _named_homes(tmp_path: Path) -> list[str]:
return [info.name for info in list_profiles() if not info.is_default]
def _delete(name: str) -> None:
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
"hermes_cli.profiles._stop_profile_backends"
):
delete_profile(name, yes=True)
class TestDeletedProfileTombstone:
def test_delete_then_logging_setup_does_not_recreate_home(self, profile_env, monkeypatch):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
"hermes_cli.profiles._stop_profile_backends"
):
delete_profile("worker", yes=True)
assert not profile_dir.exists()
assert "worker" not in _named_homes(profile_env)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
with pytest.raises(FileNotFoundError, match="Named profile home does not exist"):
setup_logging(hermes_home=profile_dir, force=True)
assert not profile_dir.exists()
monkeypatch.setenv("HERMES_HOME", str(profile_env / ".hermes"))
assert "worker" not in _named_homes(profile_env)
def test_empty_shell_after_delete_is_not_listed_or_served(self, profile_env):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
"hermes_cli.profiles._stop_profile_backends"
):
delete_profile("worker", yes=True)
# Simulate a stale mkdir that only rebuilds the directory itself.
profile_dir.mkdir(parents=True)
(profile_dir / "state.db").write_bytes(b"")
assert "worker" not in _named_homes(profile_env)
served = [name for name, _ in profiles_to_serve(True)]
assert "worker" not in served
def test_tombstoned_home_is_not_bootstrapped(self, profile_env, monkeypatch):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
"hermes_cli.profiles._stop_profile_backends"
):
delete_profile("worker", yes=True)
profile_dir.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
with pytest.raises(FileNotFoundError, match="Named profile home does not exist"):
ensure_hermes_home()
assert not (profile_dir / "sessions").exists()
def test_create_after_delete_clears_tombstone(self, profile_env):
create_profile("worker", no_alias=True, no_skills=True)
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
"hermes_cli.profiles._stop_profile_backends"
):
delete_profile("worker", yes=True)
recreated = create_profile("worker", no_alias=True, no_skills=True)
assert recreated.is_dir()
assert "worker" in _named_homes(profile_env)
def test_profile_exists_is_false_for_tombstoned_shell(self, profile_env):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
assert profile_exists("worker") is True
_delete("worker")
profile_dir.mkdir(parents=True)
assert profile_exists("worker") is False
with pytest.raises(FileNotFoundError, match="does not exist"):
set_active_profile("worker")
with pytest.raises(FileNotFoundError, match="does not exist"):
resolve_profile_env("worker")
def test_backfill_skips_tombstoned_directory(self, profile_env):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
_delete("worker")
profile_dir.mkdir(parents=True)
(profile_env / ".hermes" / ".env").write_text(
"OPENROUTER_API_KEY=root-key\n", encoding="utf-8"
)
backfilled = backfill_profile_envs(quiet=True)
assert "worker" not in backfilled
assert not (profile_dir / ".env").exists()
def test_create_after_delete_replaces_empty_shell(self, profile_env):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
_delete("worker")
profile_dir.mkdir(parents=True)
(profile_dir / "logs").mkdir()
recreated = create_profile("worker", no_alias=True, no_skills=True)
assert recreated.is_dir()
assert "worker" in _named_homes(profile_env)
@pytest.mark.parametrize("leftover", ["config.yaml", ".env"])
def test_create_after_delete_refuses_when_identity_files_remain(
self, profile_env, leftover
):
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
_delete("worker")
profile_dir.mkdir(parents=True)
leftover_path = profile_dir / leftover
leftover_path.write_text("keep-me\n", encoding="utf-8")
with pytest.raises(FileExistsError, match="already exists"):
create_profile("worker", no_alias=True, no_skills=True)
assert leftover_path.read_text(encoding="utf-8") == "keep-me\n"
assert leftover_path.exists()
class TestNamedProfileHome:
def test_logs_under_named_profile_resolve_to_profile_home(self, tmp_path):
# tmp_path acts as a real Hermes home (Docker/custom layout): it
# carries a home marker file, so profiles/ under it is canonical.
(tmp_path / "config.yaml").write_text("{}\n", encoding="utf-8")
worker = tmp_path / "profiles" / "worker"
assert named_profile_home(worker / "logs") == worker
assert named_profile_home(worker) == worker
def test_dot_hermes_layout_resolves_without_markers(self, tmp_path):
worker = tmp_path / ".hermes" / "profiles" / "worker"
assert named_profile_home(worker / "logs") == worker
assert named_profile_home(worker) == worker
def test_default_home_with_profiles_in_path_is_not_named(self, tmp_path):
default_home = tmp_path / "foo" / "profiles" / "notahome" / ".hermes"
assert named_profile_home(default_home) is None
assert named_profile_home(default_home / "logs") is None
def test_unrelated_profiles_dir_is_not_named(self, tmp_path):
# Review point 1 regression: a custom home like
# /srv/profiles/buildcache must NOT be treated as a named profile —
# its parent is not a Hermes home, so logging must keep mkdir-ing.
custom_home = tmp_path / "srv" / "profiles" / "buildcache"
assert named_profile_home(custom_home) is None
assert named_profile_home(custom_home / "logs") is None
def test_unrelated_profiles_dir_still_mkdirs(self, tmp_path):
from hermes_constants import mkdir_under_hermes_home
custom_home = tmp_path / "srv" / "profiles" / "buildcache"
log_dir = mkdir_under_hermes_home(custom_home / "logs")
assert log_dir.is_dir()
def test_setup_logging_under_unrelated_profiles_dir_succeeds(self, tmp_path):
# End-to-end shape of the point-1 regression: setup_logging on a
# non-profile custom home whose path contains a 'profiles' segment
# must create the log dir instead of raising FileNotFoundError.
custom_home = tmp_path / "srv" / "profiles" / "buildcache"
custom_home.mkdir(parents=True)
log_dir = setup_logging(hermes_home=custom_home, force=True)
assert log_dir == custom_home / "logs"
assert log_dir.is_dir()
def test_tombstone_dir_marks_profiles_root(self, tmp_path):
# A profiles/.deleted directory is only ever created by
# `hermes profile delete` — its presence alone anchors recognition,
# so tombstones are honored even when root markers are missing.
profiles_dir = tmp_path / "opt" / "profiles"
(profiles_dir / ".deleted").mkdir(parents=True)
worker = profiles_dir / "worker"
assert named_profile_home(worker / "logs") == worker