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
|
"""Atomically persist a cache file (creating parents). Raises on failure — callers decide
|
||||||
whether a failed cache write is worth logging."""
|
whether a failed cache write is worth logging."""
|
||||||
from utils import atomic_json_write
|
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)
|
atomic_json_write(path, data, **dump_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ from hermes_cli.profiles import (
|
|||||||
resolve_profile_env,
|
resolve_profile_env,
|
||||||
set_active_profile,
|
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
|
from hermes_logging import setup_logging
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +71,43 @@ class TestDeletedProfileTombstone:
|
|||||||
monkeypatch.setenv("HERMES_HOME", str(profile_env / ".hermes"))
|
monkeypatch.setenv("HERMES_HOME", str(profile_env / ".hermes"))
|
||||||
assert "worker" not in _named_homes(profile_env)
|
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):
|
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)
|
profile_dir = create_profile("worker", no_alias=True, no_skills=True)
|
||||||
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
|
with patch("hermes_cli.profiles._cleanup_gateway_service"), patch(
|
||||||
|
|||||||
@@ -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
|
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.
|
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():
|
if mode is None and not path.exists():
|
||||||
mode = default_new_file_mode()
|
mode = default_new_file_mode()
|
||||||
original_owner = _preserve_file_owner(path) if preserve_owner else None
|
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
|
from hermes_cli.config import _greedy_literal_match, _split_key_path
|
||||||
|
|
||||||
path = Path(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)
|
yaml_rt, config = _roundtrip_load(path)
|
||||||
current = config
|
current = config
|
||||||
keys = _split_key_path(key_path)
|
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
|
from hermes_cli.config import require_readable_config_before_write
|
||||||
|
|
||||||
path = Path(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)
|
||||||
require_readable_config_before_write(path)
|
require_readable_config_before_write(path)
|
||||||
yaml_rt, existing = _roundtrip_load(path)
|
yaml_rt, existing = _roundtrip_load(path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user