fix(config): respect quoted false for session vacuum_after_prune

This commit is contained in:
hharry11 2026-04-25 02:24:14 +03:00
parent 4fade39c90
commit 7da299ddb0
6 changed files with 94 additions and 15 deletions

4
cli.py
View file

@ -77,7 +77,7 @@ _COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧
# User-managed env files should override stale shell exports on restart. # User-managed env files should override stale shell exports on restart.
from hermes_constants import get_hermes_home, display_hermes_home from hermes_constants import get_hermes_home, display_hermes_home
from hermes_cli.env_loader import load_hermes_dotenv from hermes_cli.env_loader import load_hermes_dotenv
from utils import base_url_host_matches from utils import base_url_host_matches, coerce_bool
_hermes_home = get_hermes_home() _hermes_home = get_hermes_home()
_project_env = Path(__file__).parent / '.env' _project_env = Path(__file__).parent / '.env'
@ -974,7 +974,7 @@ def _run_state_db_auto_maintenance(session_db) -> None:
session_db.maybe_auto_prune_and_vacuum( session_db.maybe_auto_prune_and_vacuum(
retention_days=int(cfg.get("retention_days", 90)), retention_days=int(cfg.get("retention_days", 90)),
min_interval_hours=int(cfg.get("min_interval_hours", 24)), min_interval_hours=int(cfg.get("min_interval_hours", 24)),
vacuum=bool(cfg.get("vacuum_after_prune", True)), vacuum=coerce_bool(cfg.get("vacuum_after_prune", True), default=True),
) )
except Exception as exc: except Exception as exc:
logger.debug("state.db auto-maintenance skipped: %s", exc) logger.debug("state.db auto-maintenance skipped: %s", exc)

View file

@ -17,23 +17,14 @@ from typing import Dict, List, Optional, Any
from enum import Enum from enum import Enum
from hermes_cli.config import get_hermes_home from hermes_cli.config import get_hermes_home
from utils import is_truthy_value from utils import coerce_bool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _coerce_bool(value: Any, default: bool = True) -> bool: def _coerce_bool(value: Any, default: bool = True) -> bool:
"""Coerce bool-ish config values, preserving a caller-provided default.""" """Coerce bool-ish config values, preserving a caller-provided default."""
if value is None: return coerce_bool(value, default=default)
return default
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in ("true", "1", "yes", "on"):
return True
if lowered in ("false", "0", "no", "off"):
return False
return default
return is_truthy_value(value, default=default)
def _normalize_unauthorized_dm_behavior(value: Any, default: str = "pair") -> str: def _normalize_unauthorized_dm_behavior(value: Any, default: str = "pair") -> str:

View file

@ -89,7 +89,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
# Resolve Hermes home directory (respects HERMES_HOME override) # Resolve Hermes home directory (respects HERMES_HOME override)
from hermes_constants import get_hermes_home from hermes_constants import get_hermes_home
from utils import atomic_yaml_write, base_url_host_matches, is_truthy_value from utils import atomic_yaml_write, base_url_host_matches, coerce_bool, is_truthy_value
_hermes_home = get_hermes_home() _hermes_home = get_hermes_home()
# Load environment variables from ~/.hermes/.env first. # Load environment variables from ~/.hermes/.env first.
@ -748,7 +748,7 @@ class GatewayRunner:
self._session_db.maybe_auto_prune_and_vacuum( self._session_db.maybe_auto_prune_and_vacuum(
retention_days=int(_sess_cfg.get("retention_days", 90)), retention_days=int(_sess_cfg.get("retention_days", 90)),
min_interval_hours=int(_sess_cfg.get("min_interval_hours", 24)), min_interval_hours=int(_sess_cfg.get("min_interval_hours", 24)),
vacuum=bool(_sess_cfg.get("vacuum_after_prune", True)), vacuum=coerce_bool(_sess_cfg.get("vacuum_after_prune", True), default=True),
) )
except Exception as exc: except Exception as exc:
logger.debug("state.db auto-maintenance skipped: %s", exc) logger.debug("state.db auto-maintenance skipped: %s", exc)

View file

@ -0,0 +1,31 @@
from unittest.mock import Mock
import pytest
@pytest.mark.parametrize("raw_value", ["false", False])
def test_run_state_db_auto_maintenance_respects_vacuum_flag(monkeypatch, tmp_path, raw_value):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
import cli as cli_mod
session_db = Mock()
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {
"sessions": {
"auto_prune": True,
"retention_days": 30,
"min_interval_hours": 12,
"vacuum_after_prune": raw_value,
}
},
)
cli_mod._run_state_db_auto_maintenance(session_db)
session_db.maybe_auto_prune_and_vacuum.assert_called_once_with(
retention_days=30,
min_interval_hours=12,
vacuum=False,
)

View file

@ -0,0 +1,34 @@
from unittest.mock import Mock
import pytest
from gateway.config import GatewayConfig
from gateway.run import GatewayRunner
@pytest.mark.parametrize("raw_value", ["false", False])
def test_gateway_runner_respects_vacuum_after_prune_flag(monkeypatch, tmp_path, raw_value):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
fake_db = Mock()
monkeypatch.setattr("hermes_state.SessionDB", lambda: fake_db)
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {
"sessions": {
"auto_prune": True,
"retention_days": 45,
"min_interval_hours": 6,
"vacuum_after_prune": raw_value,
}
},
)
runner = GatewayRunner(GatewayConfig(sessions_dir=tmp_path / "sessions"))
assert runner._session_db is fake_db
fake_db.maybe_auto_prune_and_vacuum.assert_called_once_with(
retention_days=45,
min_interval_hours=6,
vacuum=False,
)

View file

@ -15,6 +15,7 @@ logger = logging.getLogger(__name__)
TRUTHY_STRINGS = frozenset({"1", "true", "yes", "on"}) TRUTHY_STRINGS = frozenset({"1", "true", "yes", "on"})
FALSY_STRINGS = frozenset({"0", "false", "no", "off"})
def is_truthy_value(value: Any, default: bool = False) -> bool: def is_truthy_value(value: Any, default: bool = False) -> bool:
@ -28,6 +29,28 @@ def is_truthy_value(value: Any, default: bool = False) -> bool:
return bool(value) return bool(value)
def coerce_bool(value: Any, default: bool = False) -> bool:
"""Coerce bool-ish config values while preserving the caller's default.
Unlike ``bool(value)``, this treats quoted config strings like
``"false"`` and ``"0"`` as ``False`` instead of truthy non-empty
strings. Unrecognized strings fall back to ``default`` so malformed
YAML values don't silently flip behavior.
"""
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in TRUTHY_STRINGS:
return True
if lowered in FALSY_STRINGS:
return False
return default
return bool(value)
def env_var_enabled(name: str, default: str = "") -> bool: def env_var_enabled(name: str, default: str = "") -> bool:
"""Return True when an environment variable is set to a truthy value.""" """Return True when an environment variable is set to a truthy value."""
return is_truthy_value(os.getenv(name, default), default=False) return is_truthy_value(os.getenv(name, default), default=False)