diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 1f0ea8dd1d..b98d30bf8d 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -7404,11 +7404,8 @@ def _cmd_update_impl(args, gateway_mode: bool): .lower() ) elif not (sys.stdin.isatty() and sys.stdout.isatty()): - print(" ℹ Non-interactive session — skipping config migration prompt.") - print( - " Run 'hermes config migrate' later to apply any new config/env options." - ) - response = "n" + print(" ℹ Non-interactive session — applying safe config migrations.") + response = "auto" else: try: response = ( @@ -7419,19 +7416,22 @@ def _cmd_update_impl(args, gateway_mode: bool): except EOFError: response = "n" - if response in ("", "y", "yes"): + if response in ("", "y", "yes", "auto"): print() - # In gateway mode OR under --yes, run auto-migrations only (no - # input() prompts for API keys which would hang the detached - # process / defeat the point of --yes). - results = migrate_config( - interactive=not (gateway_mode or assume_yes), quiet=False + # Gateway mode, --yes, and non-interactive update contexts + # (dashboard / web server actions) cannot prompt for API keys. + # Still run the non-interactive migration pass before restarting + # so new default config fields and version bumps are written + # 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"]: print() 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") else: print() diff --git a/tests/hermes_cli/test_cmd_update.py b/tests/hermes_cli/test_cmd_update.py index 57a671beab..17ab2956be 100644 --- a/tests/hermes_cli/test_cmd_update.py +++ b/tests/hermes_cli/test_cmd_update.py @@ -143,14 +143,18 @@ class TestCmdUpdateBranchFallback: (["/usr/bin/npm", "run", "build"], PROJECT_ROOT / "web"), ] - def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys): - """When stdin/stdout aren't TTYs, config migration prompt is skipped.""" + def test_update_non_interactive_runs_safe_config_migrations(self, mock_args, capsys): + """Dashboard/web updates apply non-interactive migrations before restart.""" with patch("shutil.which", return_value=None), patch( "subprocess.run" ) as mock_run, patch("builtins.input") as mock_input, patch( "hermes_cli.config.get_missing_env_vars", return_value=["MISSING_KEY"] - ), patch("hermes_cli.config.get_missing_config_fields", return_value=[]), patch( - "hermes_cli.config.check_config_version", return_value=(1, 2) + ), patch( + "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: mock_sys.stdin.isatty.return_value = False mock_sys.stdout.isatty.return_value = False @@ -161,8 +165,12 @@ class TestCmdUpdateBranchFallback: cmd_update(mock_args) 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() - 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: