fix: add --yes flag to bypass confirmation in /skills install and uninstall (#1647)
Fixes hanging when using /skills install or /skills uninstall from the TUI — bare input() calls hang inside prompt_toolkit's event loop. Changes: - Add skip_confirm parameter to do_install() and do_uninstall() - Separate --yes/-y (confirmation bypass) from --force (scan override) in both argparse and slash command handlers - Update usage hint for /skills uninstall to show [--yes] The original PR (#1595) accidentally deleted the install_from_quarantine() call, which would have broken all installs. That bug is not present here. Based on PR #1595 by 333Alden333. Co-authored-by: 333Alden333 <333Alden333@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
"""
|
||||
Tests for --yes / --force flag separation in `hermes skills install`.
|
||||
|
||||
--yes / -y → skip_confirm (bypass interactive prompt, needed in TUI mode)
|
||||
--force → force (install despite blocked scan verdict)
|
||||
|
||||
Based on PR #1595 by 333Alden333 (salvaged).
|
||||
"""
|
||||
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def test_cli_skills_install_accepts_yes_alias(monkeypatch):
|
||||
def test_cli_skills_install_yes_sets_skip_confirm(monkeypatch):
|
||||
"""--yes should set skip_confirm=True but NOT force."""
|
||||
from hermes_cli.main import main
|
||||
|
||||
captured = {}
|
||||
@@ -10,6 +20,7 @@ def test_cli_skills_install_accepts_yes_alias(monkeypatch):
|
||||
def fake_skills_command(args):
|
||||
captured["identifier"] = args.identifier
|
||||
captured["force"] = args.force
|
||||
captured["yes"] = args.yes
|
||||
|
||||
monkeypatch.setattr("hermes_cli.skills_hub.skills_command", fake_skills_command)
|
||||
monkeypatch.setattr(
|
||||
@@ -20,7 +31,98 @@ def test_cli_skills_install_accepts_yes_alias(monkeypatch):
|
||||
|
||||
main()
|
||||
|
||||
assert captured == {
|
||||
"identifier": "official/email/agentmail",
|
||||
"force": True,
|
||||
}
|
||||
assert captured["identifier"] == "official/email/agentmail"
|
||||
assert captured["yes"] is True
|
||||
assert captured["force"] is False
|
||||
|
||||
|
||||
def test_cli_skills_install_y_alias(monkeypatch):
|
||||
"""-y should behave the same as --yes."""
|
||||
from hermes_cli.main import main
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_skills_command(args):
|
||||
captured["yes"] = args.yes
|
||||
captured["force"] = args.force
|
||||
|
||||
monkeypatch.setattr("hermes_cli.skills_hub.skills_command", fake_skills_command)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["hermes", "skills", "install", "test/skill", "-y"],
|
||||
)
|
||||
|
||||
main()
|
||||
|
||||
assert captured["yes"] is True
|
||||
assert captured["force"] is False
|
||||
|
||||
|
||||
def test_cli_skills_install_force_sets_force(monkeypatch):
|
||||
"""--force should set force=True but NOT yes."""
|
||||
from hermes_cli.main import main
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_skills_command(args):
|
||||
captured["force"] = args.force
|
||||
captured["yes"] = args.yes
|
||||
|
||||
monkeypatch.setattr("hermes_cli.skills_hub.skills_command", fake_skills_command)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["hermes", "skills", "install", "test/skill", "--force"],
|
||||
)
|
||||
|
||||
main()
|
||||
|
||||
assert captured["force"] is True
|
||||
assert captured["yes"] is False
|
||||
|
||||
|
||||
def test_cli_skills_install_force_and_yes_together(monkeypatch):
|
||||
"""--force --yes should set both flags."""
|
||||
from hermes_cli.main import main
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_skills_command(args):
|
||||
captured["force"] = args.force
|
||||
captured["yes"] = args.yes
|
||||
|
||||
monkeypatch.setattr("hermes_cli.skills_hub.skills_command", fake_skills_command)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["hermes", "skills", "install", "test/skill", "--force", "--yes"],
|
||||
)
|
||||
|
||||
main()
|
||||
|
||||
assert captured["force"] is True
|
||||
assert captured["yes"] is True
|
||||
|
||||
|
||||
def test_cli_skills_install_no_flags(monkeypatch):
|
||||
"""Without flags, both force and yes should be False."""
|
||||
from hermes_cli.main import main
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_skills_command(args):
|
||||
captured["force"] = args.force
|
||||
captured["yes"] = args.yes
|
||||
|
||||
monkeypatch.setattr("hermes_cli.skills_hub.skills_command", fake_skills_command)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["hermes", "skills", "install", "test/skill"],
|
||||
)
|
||||
|
||||
main()
|
||||
|
||||
assert captured["force"] is False
|
||||
assert captured["yes"] is False
|
||||
|
||||
132
tests/hermes_cli/test_skills_skip_confirm.py
Normal file
132
tests/hermes_cli/test_skills_skip_confirm.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Tests for skip_confirm behavior in /skills install and /skills uninstall.
|
||||
|
||||
Verifies that --yes / -y bypasses the interactive confirmation prompt
|
||||
that hangs inside prompt_toolkit's TUI.
|
||||
|
||||
Based on PR #1595 by 333Alden333 (salvaged).
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHandleSkillsSlashInstallFlags:
|
||||
"""Test flag parsing in handle_skills_slash for install."""
|
||||
|
||||
def test_yes_flag_sets_skip_confirm(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_install") as mock_install:
|
||||
handle_skills_slash("/skills install test/skill --yes")
|
||||
mock_install.assert_called_once()
|
||||
_, kwargs = mock_install.call_args
|
||||
assert kwargs.get("skip_confirm") is True
|
||||
assert kwargs.get("force") is False
|
||||
|
||||
def test_y_flag_sets_skip_confirm(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_install") as mock_install:
|
||||
handle_skills_slash("/skills install test/skill -y")
|
||||
mock_install.assert_called_once()
|
||||
_, kwargs = mock_install.call_args
|
||||
assert kwargs.get("skip_confirm") is True
|
||||
|
||||
def test_force_flag_sets_force_not_skip(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_install") as mock_install:
|
||||
handle_skills_slash("/skills install test/skill --force")
|
||||
mock_install.assert_called_once()
|
||||
_, kwargs = mock_install.call_args
|
||||
assert kwargs.get("force") is True
|
||||
assert kwargs.get("skip_confirm") is False
|
||||
|
||||
def test_no_flags(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_install") as mock_install:
|
||||
handle_skills_slash("/skills install test/skill")
|
||||
mock_install.assert_called_once()
|
||||
_, kwargs = mock_install.call_args
|
||||
assert kwargs.get("force") is False
|
||||
assert kwargs.get("skip_confirm") is False
|
||||
|
||||
|
||||
class TestHandleSkillsSlashUninstallFlags:
|
||||
"""Test flag parsing in handle_skills_slash for uninstall."""
|
||||
|
||||
def test_yes_flag_sets_skip_confirm(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
|
||||
handle_skills_slash("/skills uninstall test-skill --yes")
|
||||
mock_uninstall.assert_called_once()
|
||||
_, kwargs = mock_uninstall.call_args
|
||||
assert kwargs.get("skip_confirm") is True
|
||||
|
||||
def test_y_flag_sets_skip_confirm(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
|
||||
handle_skills_slash("/skills uninstall test-skill -y")
|
||||
mock_uninstall.assert_called_once()
|
||||
_, kwargs = mock_uninstall.call_args
|
||||
assert kwargs.get("skip_confirm") is True
|
||||
|
||||
def test_no_flags(self):
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
|
||||
handle_skills_slash("/skills uninstall test-skill")
|
||||
mock_uninstall.assert_called_once()
|
||||
_, kwargs = mock_uninstall.call_args
|
||||
assert kwargs.get("skip_confirm", False) is False
|
||||
|
||||
|
||||
class TestDoInstallSkipConfirm:
|
||||
"""Test that do_install respects skip_confirm parameter."""
|
||||
|
||||
@patch("hermes_cli.skills_hub.input", return_value="n")
|
||||
def test_without_skip_confirm_prompts_user(self, mock_input):
|
||||
"""Without skip_confirm, input() is called for confirmation."""
|
||||
from hermes_cli.skills_hub import do_install
|
||||
with patch("hermes_cli.skills_hub._console"), \
|
||||
patch("tools.skills_hub.ensure_hub_dirs"), \
|
||||
patch("tools.skills_hub.GitHubAuth"), \
|
||||
patch("tools.skills_hub.create_source_router") as mock_router, \
|
||||
patch("hermes_cli.skills_hub._resolve_short_name", return_value="test/skill"), \
|
||||
patch("hermes_cli.skills_hub._resolve_source_meta_and_bundle") as mock_resolve:
|
||||
|
||||
# Make it return None so we exit early
|
||||
mock_resolve.return_value = (None, None, None)
|
||||
do_install("test-skill", skip_confirm=False)
|
||||
# We don't get to the input() call because resolve returns None,
|
||||
# but the parameter wiring is correct
|
||||
|
||||
|
||||
class TestDoUninstallSkipConfirm:
|
||||
"""Test that do_uninstall respects skip_confirm parameter."""
|
||||
|
||||
def test_skip_confirm_bypasses_input(self):
|
||||
"""With skip_confirm=True, input() should not be called."""
|
||||
from hermes_cli.skills_hub import do_uninstall
|
||||
with patch("hermes_cli.skills_hub._console") as mock_console, \
|
||||
patch("tools.skills_hub.uninstall_skill", return_value=(True, "Removed")) as mock_uninstall, \
|
||||
patch("builtins.input") as mock_input:
|
||||
do_uninstall("test-skill", skip_confirm=True)
|
||||
mock_input.assert_not_called()
|
||||
mock_uninstall.assert_called_once_with("test-skill")
|
||||
|
||||
def test_without_skip_confirm_calls_input(self):
|
||||
"""Without skip_confirm, input() should be called."""
|
||||
from hermes_cli.skills_hub import do_uninstall
|
||||
with patch("hermes_cli.skills_hub._console"), \
|
||||
patch("tools.skills_hub.uninstall_skill", return_value=(True, "Removed")), \
|
||||
patch("builtins.input", return_value="y") as mock_input:
|
||||
do_uninstall("test-skill", skip_confirm=False)
|
||||
mock_input.assert_called_once()
|
||||
|
||||
def test_without_skip_confirm_cancel(self):
|
||||
"""Without skip_confirm, answering 'n' should cancel."""
|
||||
from hermes_cli.skills_hub import do_uninstall
|
||||
with patch("hermes_cli.skills_hub._console"), \
|
||||
patch("tools.skills_hub.uninstall_skill") as mock_uninstall, \
|
||||
patch("builtins.input", return_value="n"):
|
||||
do_uninstall("test-skill", skip_confirm=False)
|
||||
mock_uninstall.assert_not_called()
|
||||
Reference in New Issue
Block a user