Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
"""Tests for `hermes update --yes / -y` — assume yes for interactive prompts.
|
||||
|
||||
Covers:
|
||||
1. argparse parses the flag
|
||||
2. Config-migration prompt is auto-answered (no input() call) and migrate_config
|
||||
runs with interactive=False so API-key prompts are skipped
|
||||
3. Autostash restore prompt is auto-answered (prompt_for_restore == False, no
|
||||
input() call) and the stash is applied automatically
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from hermes_cli.main import cmd_update
|
||||
|
||||
|
||||
def _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="1", dirty=False
|
||||
):
|
||||
"""Minimal subprocess.run side_effect for the update flow."""
|
||||
|
||||
def side_effect(cmd, **kwargs):
|
||||
joined = " ".join(str(c) for c in cmd)
|
||||
|
||||
if "rev-parse" in joined and "--abbrev-ref" in joined:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout=f"{branch}\n", stderr="")
|
||||
if "rev-parse" in joined and "--verify" in joined:
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0 if verify_ok else 128, stdout="", stderr=""
|
||||
)
|
||||
if "rev-list" in joined:
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0, stdout=f"{commit_count}\n", stderr=""
|
||||
)
|
||||
# `git status --porcelain` for dirty-tree detection during autostash.
|
||||
if "status" in joined and "--porcelain" in joined:
|
||||
out = " M hermes_cli/main.py\n" if dirty else ""
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout=out, stderr="")
|
||||
# `git stash list` — return a stash ref when dirty (so _stash_local_changes
|
||||
# gets something to return). _stash_local_changes_if_needed is what we
|
||||
# actually patch in tests that exercise restore, so this is a catch-all.
|
||||
if "stash" in joined and "list" in joined:
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||
|
||||
return side_effect
|
||||
|
||||
|
||||
class TestUpdateYesConfigMigration:
|
||||
"""--yes auto-answers the config-migration prompt and skips API-key prompts."""
|
||||
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@patch("hermes_cli.config.get_missing_env_vars", return_value=["NEW_KEY"])
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_yes_auto_migrates_without_input(
|
||||
self,
|
||||
mock_run,
|
||||
_mock_which,
|
||||
_mock_missing_env,
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="1"
|
||||
)
|
||||
mock_migrate.return_value = {"env_added": [], "config_added": []}
|
||||
|
||||
args = SimpleNamespace(yes=True)
|
||||
|
||||
with patch("builtins.input") as mock_input:
|
||||
cmd_update(args)
|
||||
# Never prompted the user.
|
||||
mock_input.assert_not_called()
|
||||
|
||||
# migrate_config was invoked with interactive=False — API-key prompts
|
||||
# are suppressed, matching gateway-mode semantics.
|
||||
assert mock_migrate.call_count == 1
|
||||
_, kwargs = mock_migrate.call_args
|
||||
assert kwargs.get("interactive") is False
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "--yes: auto-applying config migration" in out
|
||||
# The "Would you like to configure them now?" prompt text never appears.
|
||||
assert "Would you like to configure them now?" not in out
|
||||
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@patch("hermes_cli.config.get_missing_env_vars", return_value=["NEW_KEY"])
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_no_yes_flag_still_prompts_in_tty(
|
||||
self,
|
||||
mock_run,
|
||||
_mock_which,
|
||||
_mock_missing_env,
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
"""Regression guard: without --yes, the TTY prompt path still fires."""
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="1"
|
||||
)
|
||||
mock_migrate.return_value = {"env_added": [], "config_added": []}
|
||||
|
||||
args = SimpleNamespace(yes=False)
|
||||
|
||||
# Patch ``sys.stdin.isatty`` and ``sys.stdout.isatty`` directly on the
|
||||
# real ``sys`` module instead of replacing ``hermes_cli.main.sys`` with
|
||||
# a MagicMock. The MagicMock approach was flaky under ``pytest-xdist``
|
||||
# — a sibling test that imported ``hermes_cli.main`` first could leave
|
||||
# a different ``sys`` reference resolved inside the function and the
|
||||
# mock would never be consulted, with CI then taking the
|
||||
# "Non-interactive session" branch instead of prompting.
|
||||
import sys as _sys
|
||||
|
||||
with patch("builtins.input", return_value="n") as mock_input, patch.object(
|
||||
_sys.stdin, "isatty", return_value=True
|
||||
), patch.object(_sys.stdout, "isatty", return_value=True):
|
||||
cmd_update(args)
|
||||
# The user was actually prompted.
|
||||
assert mock_input.called
|
||||
prompts = [c.args[0] if c.args else "" for c in mock_input.call_args_list]
|
||||
assert any("configure them now" in p for p in prompts)
|
||||
|
||||
|
||||
class TestUpdateYesStashRestore:
|
||||
"""--yes auto-restores the pre-update autostash without prompting."""
|
||||
|
||||
|
||||
|
||||
class TestUnicodeDecodeErrorInUpdatePrompts:
|
||||
"""Regression tests (review of #68497): input() can raise
|
||||
UnicodeDecodeError when the terminal encoding can't decode the byte
|
||||
sequence (e.g. a non-UTF-8 locale, or an embedded terminal). Three
|
||||
interactive update prompts call input() directly -- the config-
|
||||
migration prompt, the stash-restore prompt, and the upstream-remote
|
||||
prompt -- and each must fail safe (skip, don't crash) rather than let
|
||||
the exception escape and crash `hermes update` mid-flight.
|
||||
"""
|
||||
|
||||
@patch("hermes_cli.update_cmd._reload_config_modules")
|
||||
@patch("hermes_cli.update_cmd._run_migrate_config_fresh")
|
||||
@patch("hermes_cli.update_cmd._run_config_check_fresh", return_value=(1, 2))
|
||||
@patch("hermes_cli.config.get_missing_config_fields", return_value=[])
|
||||
@patch("hermes_cli.config.get_missing_env_vars", return_value=["NEW_KEY"])
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("subprocess.run")
|
||||
def test_unicode_decode_error_in_tty_skips_and_prints_hint(
|
||||
self,
|
||||
mock_run,
|
||||
_mock_which,
|
||||
_mock_missing_env,
|
||||
_mock_missing_cfg,
|
||||
_mock_version,
|
||||
mock_migrate,
|
||||
_mock_reload,
|
||||
capsys,
|
||||
):
|
||||
mock_run.side_effect = _make_run_side_effect(
|
||||
branch="main", verify_ok=True, commit_count="1"
|
||||
)
|
||||
mock_migrate.return_value = {"env_added": [], "config_added": []}
|
||||
args = SimpleNamespace(yes=False)
|
||||
|
||||
import sys as _sys
|
||||
|
||||
with patch(
|
||||
"builtins.input",
|
||||
side_effect=UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid byte"),
|
||||
), patch.object(_sys.stdin, "isatty", return_value=True), patch.object(
|
||||
_sys.stdout, "isatty", return_value=True
|
||||
):
|
||||
cmd_update(args) # must not raise
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "hermes config migrate" in out
|
||||
mock_migrate.assert_not_called()
|
||||
|
||||
def test_stash_restore_unicode_decode_error_falls_through_to_skip(self, tmp_path, capsys):
|
||||
from hermes_cli.update_cmd import _restore_stashed_changes
|
||||
|
||||
with patch(
|
||||
"builtins.input",
|
||||
side_effect=UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid byte"),
|
||||
):
|
||||
result = _restore_stashed_changes(
|
||||
["git"], tmp_path, "stash@{0}", prompt_user=True, input_fn=None,
|
||||
) # must not raise
|
||||
|
||||
assert result is False
|
||||
out = capsys.readouterr().out
|
||||
assert "Skipped restoring local changes" in out
|
||||
assert "git stash apply stash@{0}" in out
|
||||
|
||||
def test_stash_restore_eof_error_still_falls_through_to_skip(self, tmp_path):
|
||||
"""Sanity: this fix must not regress the pre-existing EOFError case,
|
||||
which the raw input() path had no guard for at all before this fix."""
|
||||
from hermes_cli.update_cmd import _restore_stashed_changes
|
||||
|
||||
with patch("builtins.input", side_effect=EOFError()):
|
||||
result = _restore_stashed_changes(
|
||||
["git"], tmp_path, "stash@{0}", prompt_user=True, input_fn=None,
|
||||
) # must not raise
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_upstream_remote_prompt_unicode_decode_error_falls_through_to_skip(
|
||||
self, tmp_path
|
||||
):
|
||||
from hermes_cli.update_cmd import _sync_with_upstream_if_needed
|
||||
|
||||
with patch(
|
||||
"hermes_cli.update_cmd._has_upstream_remote", return_value=False
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._should_skip_upstream_prompt", return_value=False
|
||||
), patch(
|
||||
"hermes_cli.update_cmd._add_upstream_remote"
|
||||
) as mock_add, patch(
|
||||
"builtins.input",
|
||||
side_effect=UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid byte"),
|
||||
):
|
||||
_sync_with_upstream_if_needed(["git"], tmp_path) # must not raise
|
||||
|
||||
mock_add.assert_not_called()
|
||||
Reference in New Issue
Block a user