fix(update): migrate config in non-interactive updates

This commit is contained in:
Steven Chou 2026-05-03 21:10:42 +08:00 committed by Teknium
parent 84287b0de8
commit 9442a8fa22
2 changed files with 25 additions and 17 deletions

View file

@ -7404,11 +7404,8 @@ def _cmd_update_impl(args, gateway_mode: bool):
.lower() .lower()
) )
elif not (sys.stdin.isatty() and sys.stdout.isatty()): elif not (sys.stdin.isatty() and sys.stdout.isatty()):
print(" Non-interactive session — skipping config migration prompt.") print(" Non-interactive session — applying safe config migrations.")
print( response = "auto"
" Run 'hermes config migrate' later to apply any new config/env options."
)
response = "n"
else: else:
try: try:
response = ( response = (
@ -7419,19 +7416,22 @@ def _cmd_update_impl(args, gateway_mode: bool):
except EOFError: except EOFError:
response = "n" response = "n"
if response in ("", "y", "yes"): if response in ("", "y", "yes", "auto"):
print() print()
# In gateway mode OR under --yes, run auto-migrations only (no # Gateway mode, --yes, and non-interactive update contexts
# input() prompts for API keys which would hang the detached # (dashboard / web server actions) cannot prompt for API keys.
# process / defeat the point of --yes). # Still run the non-interactive migration pass before restarting
results = migrate_config( # so new default config fields and version bumps are written
interactive=not (gateway_mode or assume_yes), quiet=False # before the freshly updated gateway validates config at startup.
interactive_migration = not (
gateway_mode or assume_yes or response == "auto"
) )
results = migrate_config(interactive=interactive_migration, quiet=False)
if results["env_added"] or results["config_added"]: if results["env_added"] or results["config_added"]:
print() print()
print("✓ Configuration updated!") print("✓ Configuration updated!")
if (gateway_mode or assume_yes) and missing_env: if (gateway_mode or assume_yes or response == "auto") and missing_env:
print(" API keys require manual entry: hermes config migrate") print(" API keys require manual entry: hermes config migrate")
else: else:
print() print()

View file

@ -143,14 +143,18 @@ class TestCmdUpdateBranchFallback:
(["/usr/bin/npm", "run", "build"], PROJECT_ROOT / "web"), (["/usr/bin/npm", "run", "build"], PROJECT_ROOT / "web"),
] ]
def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys): def test_update_non_interactive_runs_safe_config_migrations(self, mock_args, capsys):
"""When stdin/stdout aren't TTYs, config migration prompt is skipped.""" """Dashboard/web updates apply non-interactive migrations before restart."""
with patch("shutil.which", return_value=None), patch( with patch("shutil.which", return_value=None), patch(
"subprocess.run" "subprocess.run"
) as mock_run, patch("builtins.input") as mock_input, patch( ) as mock_run, patch("builtins.input") as mock_input, patch(
"hermes_cli.config.get_missing_env_vars", return_value=["MISSING_KEY"] "hermes_cli.config.get_missing_env_vars", return_value=["MISSING_KEY"]
), patch("hermes_cli.config.get_missing_config_fields", return_value=[]), patch( ), patch(
"hermes_cli.config.check_config_version", return_value=(1, 2) "hermes_cli.config.get_missing_config_fields",
return_value=[{"key": "new.option", "default": True}],
), patch("hermes_cli.config.check_config_version", return_value=(1, 2)), patch(
"hermes_cli.config.migrate_config",
return_value={"env_added": [], "config_added": ["new.option"]},
), patch("hermes_cli.main.sys") as mock_sys: ), patch("hermes_cli.main.sys") as mock_sys:
mock_sys.stdin.isatty.return_value = False mock_sys.stdin.isatty.return_value = False
mock_sys.stdout.isatty.return_value = False mock_sys.stdout.isatty.return_value = False
@ -161,8 +165,12 @@ class TestCmdUpdateBranchFallback:
cmd_update(mock_args) cmd_update(mock_args)
mock_input.assert_not_called() mock_input.assert_not_called()
from hermes_cli.config import migrate_config
migrate_config.assert_called_once_with(interactive=False, quiet=False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Non-interactive session" in captured.out assert "applying safe config migrations" in captured.out
assert "API keys require manual entry" in captured.out
class TestCmdUpdateProfileSkillSync: class TestCmdUpdateProfileSkillSync: