fix: guard atomic writers after profile deletion
This commit is contained in:
@@ -103,8 +103,9 @@ def _write_json_cache(path: Path, data: Any, **dump_kwargs: Any) -> None:
|
||||
"""Atomically persist a cache file (creating parents). Raises on failure — callers decide
|
||||
whether a failed cache write is worth logging."""
|
||||
from utils import atomic_json_write
|
||||
from hermes_constants import mkdir_under_hermes_home
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
mkdir_under_hermes_home(path.parent)
|
||||
atomic_json_write(path, data, **dump_kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ from hermes_cli.profiles import (
|
||||
resolve_profile_env,
|
||||
set_active_profile,
|
||||
)
|
||||
from hermes_constants import named_profile_home
|
||||
from hermes_constants import (
|
||||
named_profile_home,
|
||||
reset_hermes_home_override,
|
||||
set_hermes_home_override,
|
||||
)
|
||||
from hermes_logging import setup_logging
|
||||
|
||||
|
||||
@@ -67,6 +71,43 @@ class TestDeletedProfileTombstone:
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile_env / ".hermes"))
|
||||
assert "worker" not in _named_homes(profile_env)
|
||||
|
||||
def test_late_reasoning_caps_save_does_not_recreate_deleted_home(self, profile_env):
|
||||
"""A daemon retaining a deleted profile context must not recreate its cache tree."""
|
||||
from hermes_cli import models_reasoning_caps
|
||||
|
||||
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
|
||||
_delete("worker")
|
||||
|
||||
token = set_hermes_home_override(profile_dir)
|
||||
try:
|
||||
models_reasoning_caps._save_reasoning_caps_disk(
|
||||
"https://example.test/v1/models",
|
||||
{"example/model": {"supports_reasoning": True}},
|
||||
)
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
assert not profile_dir.exists()
|
||||
|
||||
def test_atomic_cache_write_allows_live_profile_home(self, profile_env):
|
||||
from hermes_cli.models import _write_json_cache
|
||||
|
||||
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
|
||||
cache_path = profile_dir / "cache" / "models.json"
|
||||
|
||||
_write_json_cache(cache_path, {"cached": True})
|
||||
|
||||
assert cache_path.read_text(encoding="utf-8") == '{\n "cached": true\n}'
|
||||
|
||||
def test_atomic_cache_write_allows_unrelated_profiles_path(self, tmp_path):
|
||||
from hermes_cli.models import _write_json_cache
|
||||
|
||||
cache_path = tmp_path / "custom" / "profiles" / "cache" / "models.json"
|
||||
|
||||
_write_json_cache(cache_path, {"cached": True})
|
||||
|
||||
assert cache_path.read_text(encoding="utf-8") == '{\n "cached": true\n}'
|
||||
|
||||
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(
|
||||
|
||||
15
utils.py
15
utils.py
@@ -240,7 +240,12 @@ def _atomic_write(path: Path, write, *, prefix: str, encoding: str = "utf-8", mo
|
||||
also fsyncs the parent so the rename itself is durable. The temp file is removed on any
|
||||
failure — ``BaseException`` on purpose, so KeyboardInterrupt / SystemExit still clean up.
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# A profile delete leaves a tombstone beside its removed home. Background
|
||||
# writers may retain that home in a context variable, so a plain mkdir here
|
||||
# would resurrect the profile before the write can fail.
|
||||
from hermes_constants import mkdir_under_hermes_home
|
||||
|
||||
mkdir_under_hermes_home(path.parent)
|
||||
if mode is None and not path.exists():
|
||||
mode = default_new_file_mode()
|
||||
original_owner = _preserve_file_owner(path) if preserve_owner else None
|
||||
@@ -423,7 +428,9 @@ def atomic_roundtrip_yaml_update(path: Union[str, Path], key_path: str, value: A
|
||||
from hermes_cli.config import _greedy_literal_match, _split_key_path
|
||||
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
from hermes_constants import mkdir_under_hermes_home
|
||||
|
||||
mkdir_under_hermes_home(path.parent)
|
||||
yaml_rt, config = _roundtrip_load(path)
|
||||
current = config
|
||||
keys = _split_key_path(key_path)
|
||||
@@ -467,7 +474,9 @@ def atomic_roundtrip_yaml_save(path: Union[str, Path], new_state: dict) -> None:
|
||||
from hermes_cli.config import require_readable_config_before_write
|
||||
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
from hermes_constants import mkdir_under_hermes_home
|
||||
|
||||
mkdir_under_hermes_home(path.parent)
|
||||
require_readable_config_before_write(path)
|
||||
yaml_rt, existing = _roundtrip_load(path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user