fix: guard atomic writers after profile deletion

This commit is contained in:
KoNit-K
2026-09-16 12:34:46 +08:00
committed by Teknium
parent 81140e4546
commit 91a38622db
3 changed files with 56 additions and 5 deletions

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)